Compare commits

..
579 Commits
Author SHA1 Message Date
selimaj-dev 7b312f54ae Merge pull request 'Bump the version to 0.1.5-beta' (#53) from release-0.1.5-beta into master
Release / build (release) Successful in 4m38s
Reviewed-on: #53
2026-09-30 20:34:50 +00:00
selimaj-devandclaude a1dcd25710 Bump the version to 0.1.5-beta
Check / compile (pull_request) Successful in 2m57s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:33:22 +02:00
selimaj-dev 068c15d781 Merge pull request 'Build for Minecraft 26.3' (#52) from mc-26.3 into master
Reviewed-on: #52
2026-09-30 20:31:32 +00:00
selimaj-devandclaude 46c7bacd04 Build for Minecraft 26.3
Check / compile (pull_request) Successful in 16m19s
Adds 26.3 as a Stonecutter version, the last step of #39.

Input: 26.3 replaces GLFW with SDL, which numbers keys (scancodes),
modifiers and mouse buttons differently. Saturn keeps GLFW's numbers,
which common uses and the config's keybinds store, so saved keybinds
stay valid. The new Keys class converts at the boundary: key events,
modifiers and mouse buttons coming into Saturn's screens, and key codes
going to InputConstants. Its table pairs GLFW's codes with 26.3's
InputConstants of the same name. Key names come from the game on 26.3,
and version-specific code uses InputConstants' key constants rather
than GLFW's.

Rendering: Blaze3D's API moved to renderpearl, which Stonecutter
replacements cover. PoseStack's quaternion mulPose became rotate, and
TextureTarget takes a depth format instead of a flag. 26.3 compiles
shaders through SPIR-V and reorders the transforms block, so glass.fsh
takes explicit locations and the new order behind defines that
GlassRenderer sets, and the unlit cosmetics pipeline declares its
color target, which 26.3 requires.

OptionsScreen no longer takes whether a world is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:12:50 +02:00
selimaj-dev effdbe1c84 Merge pull request 'Build for Minecraft 26.2 and fix the glass blur on 26.x' (#51) from mc-26.2 into master
Reviewed-on: #51
2026-09-30 19:51:07 +00:00
selimaj-devandclaude 4b776663bb Point common at the removal of the old theme color upgrade
Check / compile (pull_request) Successful in 15m55s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:49:25 +02:00
selimaj-devandclaude 86f326a8b4 Blur the glass backdrop once the frame's world is drawn on 26.x
From 26.1 the GUI is built before the frame is drawn. The glass copies
the main framebuffer while the GUI is built, so it copied the last
frame, glass included, and each frame blurred the previous blur until
it bloomed.

On 26.1+ captureBackdrop now only prepares the backdrop and queues its
blur. GuiRendererMixin copies and blurs the queued backdrops as the GUI
renderer is about to draw, with the world and panorama drawn and no GUI
yet. Pending backdrops are never released before they're drawn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:46:14 +02:00
selimaj-devandclaude 6b1b79062c Note in the version guide that version blocks can nest
Stonecutter 0.9 writes an inactive block inside another as /^ ... ^/,
so nesting works (GlassRenderer relies on it since 26.1). else if is
still clearer for several ranges of the same code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:31:32 +02:00
selimaj-devandclaude a4ed30111c Build for Minecraft 26.2
Adds 26.2 as a Stonecutter version (#39).

26.2 splits the HUD out of Gui into Hud and moves the open screen, the
overlay and the toasts from Minecraft to Gui. A new Screens helper gets
and sets the screen and clears the overlay on every version, and
MinecraftClientMixin opens screens through it instead of shadowing
Minecraft.setScreen. SetScreenMixin targets Gui.setScreen and the HUD
hook Hud.extractChat on 26.2.

26.2's rendering API declares samplers through bind group layouts and
gives render targets a GpuFormat, so the glass pipeline adds SAMPLER1's
layout and its backdrops are RGBA8_UNORM like vanilla's targets. The
main render target is the game renderer's, the panorama no longer takes
a fade flag, and toasts are added with SystemToast.add.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:31:32 +02:00
selimaj-dev bd58bfd0a0 Merge pull request 'Build for Minecraft 26.1' (#50) from mc-26 into master
Reviewed-on: #50
2026-09-30 19:21:30 +00:00
selimaj-devandclaude 1fdbf82abd Draw hats and cloaks unlit on 26.1
Check / compile (pull_request) Successful in 13m28s
Before 26.1 they use the dragon's alpha pipeline, whose shader only
samples the texture. entityCutout, which 26.1 used instead, applies
the lightmap and directional shading, so shadows and light from the
world showed on them. 26.1 builds the same unlit cutout from the
entity shader: the emissive snippet without the lightmap, plus
NO_CARDINAL_LIGHTING and NO_OVERLAY.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 19:26:59 +02:00
selimaj-devandclaude 4fb48cc647 Build for Minecraft 26.1
Adds 26.1 as a Stonecutter version (step 3 of #39). Its jar also
loads on the 26.1.1 and 26.1.2 hotfixes.

Most of 26.1's changes are renames: the GUI's render methods became
extract methods and GuiGraphics became GuiGraphicsExtractor, which
Stonecutter replacements cover along with the moved render state
package and GuiRenderState's add methods. The rest are version
comments:

- The menu panorama is drawn by GuiRenderer from vanilla's cube map;
  the new GuiRendererMixin swaps in Saturn's while a Saturn screen is
  open (empty on older versions).
- The camera works out the FOV and aligns with its entity in
  alignWithEntity, so the zoom and freelook hooks move to CameraMixin.
- Gamma and the menu blur are read while extracting render state.
- The glass blur's post chain takes a Projection and its buffer.
- The dragon's alpha render type is gone; capes use entityCutout.
- Fabric API renamed KeyBindingHelper to KeyMappingHelper.

The docs describe the 26.x build and Java 25.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 19:15:10 +02:00
selimaj-devandclaude b018db1851 Build with loom-back-compat and a Java toolchain per version
Prepares the build for 26.x, which ships unobfuscated and needs Java 25
(#39):

- loom-back-compat applies the remapping Loom to 1.21.x and would apply
  the plain one to 26.x. loom_version becomes loomx.loom_version.
- Each version compiles with its Java toolchain (21 for 1.21.x, 25 for
  26.x), and the mixin configs take their compatibility level from it.
  The foojay resolver downloads a missing JDK; CI installs 21 and 25.
- buildAll collects remapJar's output, or jar's where there's nothing
  to remap.
- fabric.mod.json depends on fabric-api, Fabric API's mod id; 26.x's
  jar no longer provides the old "fabric" alias.

Nothing changes for the 1.21.x jars.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 19:15:02 +02:00
selimaj-devandclaude 950a83c48d Use emotes 0.1.2
It adds 26.1-26.3 builds and changes nothing for 1.21.x.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 19:14:46 +02:00
selimaj-dev a7b2e3be8e Merge pull request 'Build every version with Mojang mappings instead of Yarn' (#49) from mojang-mappings into master
Reviewed-on: #49
2026-09-30 16:17:05 +00:00
selimaj-devandclaude 55067e9761 Point common at its master merge commit
Check / compile (pull_request) Successful in 11m53s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 18:04:12 +02:00
selimaj-devandclaude 09bc223b8b Implement the Ref interfaces explicitly in their mixins
Several mixins implemented a Ref method only through a vanilla method
of the same name. Released jars rename vanilla methods to intermediary
names, so those Ref methods had no implementation there and threw
AbstractMethodError: the armour display's durability, the status
effects' icon and infinite checks, the SVG texture size and the
pre-1.21.6 matrix stack's push and pop. With Mojang's names the same
happens in development (1.21.4 crashed on the title screen).

Each mixin now shadows the vanilla method and implements the Ref
method by calling it. Points common at the Ref renames this needs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 17:42:48 +02:00
selimaj-devandclaude af27907356 Build every version with Mojang mappings instead of Yarn
Minecraft ships unobfuscated from 26.1, and 1.21.11 has the last Yarn
release, so the shared src/ has to use Mojang's names before newer
versions can be added (#39).

Each version's sources were migrated with Loom's migrateMappings and
merged back into src/, so code that is only active on other versions
is migrated too. Where Mojang's names differ between 1.21.x versions,
src/ uses the newest ones: Stonecutter replacements swap in the older
Identifier/ResourceLocation and Avatar/Player renderer names, and
version comments cover the few package moves and renamed methods.

The fog mixin now targets setupFog on every version. Yarn's applyFog
named two methods from 1.21.6 on, so it also applied to updateBuffer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 17:39:14 +02:00
selimaj-dev 37db94649f Merge pull request 'Bump the version to 0.1.4-beta' (#46) from release-0.1.4-beta into master
Release / build (release) Successful in 4m36s
Reviewed-on: #46
2026-09-29 16:37:19 +00:00
selimaj-devandclaude 896f4cccc0 Bump the version to 0.1.4-beta
Check / compile (pull_request) Successful in 2m29s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:37:03 +02:00
selimaj-dev de3c554e63 Merge pull request 'Hide the cloak on torso-curving emotes and point common at #12' (#45) from release-fixes into master
Reviewed-on: #45
2026-09-29 16:35:28 +00:00
selimaj-devandclaude 8417427059 Point common at the None cloak fix and the sidebar and dock order
Check / compile (pull_request) Successful in 2m29s
Picks up common#12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:35:14 +02:00
selimaj-devandclaude ba5a32fb34 Hide the cloak during emotes that curve the torso
A flat cloak can't follow an arched back, so on the twerk it cut through the player. When the
emote being played bends the torso more than 10 degrees at any point, the cloak is hidden for the
whole emote; hiding it only while the back is curved would make it flicker. Each emote is checked
once and cached. Emotes that bend the torso less still hang the cloak from the leaning chest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:34:34 +02:00
selimaj-dev 8b1c94151a Merge pull request 'Use emotes 0.1.1 and point common at the new emotes' (#44) from new-emotes into master
Reviewed-on: #44
2026-09-29 16:03:15 +00:00
selimaj-devandclaude 3623024cce Use emotes 0.1.1 and point common at the new emotes
Check / compile (pull_request) Successful in 2m30s
Emotes 0.1.1 bends the torso the right way round. common now has seven more emotes and a twerk
re-exported for that fix (common#11), so the two move together.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:02:50 +02:00
selimaj-dev 2dcbe0302e Merge pull request 'Point common at the twerk emote' (#43) from twerk-emote into master
Reviewed-on: #43
2026-09-29 15:04:46 +00:00
selimaj-devandclaude 502bddd720 Point common at the twerk emote
Check / compile (pull_request) Successful in 2m39s
Picks up the twerk emote and the removal of the front flip (common#10).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:01:52 +02:00
selimaj-devandclaude 0c22fee8c0 Point common at the opaque theme upgrade
Release / build (release) Successful in 5m26s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:12:35 +02:00
selimaj-dev f69f6eb883 Merge pull request 'Bump the version to 0.1.3-beta' (#42) from release-0.1.3-beta into master
Reviewed-on: #42
2026-09-28 21:02:25 +00:00
selimaj-devandclaude cebd9aec96 Bump the version to 0.1.3-beta
Check / compile (pull_request) Successful in 3m0s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:02:09 +02:00
selimaj-dev 2554a84950 Merge pull request 'Keep hats and cloaks on the posed model, and fix resources on 1.21.4 and 1.21.5' (#41) from cosmetics-follow-emotes into master
Reviewed-on: #41
2026-09-28 21:01:06 +00:00
selimaj-devandclaude 49bbdff418 Keep hats and cloaks on the posed head and body
Check / compile (pull_request) Successful in 2m57s
The hat only took the head's rotation from 1.21.5, and the cloak sat at
a fixed spot with a hard-coded crouch, so crouching and emotes left them
where the head and body had been. Both now follow the whole player (the
root, which emotes like the backflip turn) and then the head or body, as
vanilla places head items, and the cloak leans with the chest when an
emote bends the torso.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:00:40 +02:00
selimaj-devandclaude 8995de9067 Stop the glass shader importing what older versions lack
1.21.4 and 1.21.5 read every shader's imports while loading resources,
and dynamictransforms.glsl only exists from 1.21.6, so resources failed
to load there. The glass shader declares the transforms block itself
again, with LineWidth behind a define the pipeline sets before 1.21.11.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:00:40 +02:00
selimaj-dev 18b74212ca Merge pull request 'Add GUI scale and glass on every version' (#40) from glass-blur into master
Reviewed-on: #40
2026-09-28 20:02:49 +00:00
selimaj-devandclaude 06190d0ef9 Point common at its master merge commit
Check / compile (pull_request) Successful in 3m53s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 22:01:43 +02:00
selimaj-devandclaude 254a37d757 Skip glass and blur work that draws nothing
Before 1.21.6 the game is only saved for glass when glass can be drawn,
and a background blur of 0 no longer runs the blur. Every version frees
the copies (and from 1.21.6 the blurs) of glass strengths no longer set,
instead of keeping one per strength ever used.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 21:57:10 +02:00
selimaj-devandclaude 2b5bcf4098 Draw glass on every version
1.21.6 to 1.21.10 draw glass as 1.21.11 does, without samplers, and the
glass shader imports vanilla's transforms block, whose layout differs
between them. 1.21.4 and 1.21.5 draw straight into the framebuffer,
so the game is saved before the screen blurs it, and glass is a render
layer: with a pipeline on 1.21.5 and a shader program on 1.21.4.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 21:55:52 +02:00
selimaj-devandclaude 4e00934353 Blur each glass strength separately on 1.21.11
Every blur strength in use gets its own copy of the game, blurred with
vanilla's blur at that radius instead of the animated menu blur.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 21:35:23 +02:00
selimaj-devandclaude c524451704 Draw glass behind translucent panels on 1.21.11
A copy of the game behind the screen is blurred once a frame and drawn
through each panel's rounded rectangle texture by a glass pipeline.
Older versions keep blurring the whole background.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 20:43:44 +02:00
selimaj-devandclaude e26ce6d076 Point common at the theme alpha fix
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:56:24 +02:00
selimaj-devandclaude da3dfaee66 Point common at the faster color picker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:45:04 +02:00
selimaj-devandclaude 463335856f Point common at the title screen polish
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:39:39 +02:00
selimaj-devandclaude 3315926f7c Point common at the title screen corners
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:20:33 +02:00
selimaj-devandclaude 68d3fb6062 Point common at the title screen layout
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:09:10 +02:00
selimaj-devandclaude b108cc1f4a Scale Saturn's screens by their GUI scale setting
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:59:47 +02:00
selimaj-dev 167010380e updated screenshot 2026-09-27 19:48:09 +02:00
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
selimaj-dev 3ba9079101 Updated version 2026-03-30 13:57:59 +02:00
Kr4ight db1d31b1e3 Merge pull request #68 from saturnclientmc/improved-session-handling
Fixed large logs when session is offline #66
2026-03-30 13:56:22 +02:00
selimaj-dev d0e712a3c5 Fixed large logs when session is offline #66 2026-03-30 13:51:49 +02:00
Kr4ight b86a2775a1 Merge pull request #65 from saturnclientmc/improved-build-configs
Improved build configs
2026-03-28 21:13:48 +01:00
selimaj-dev 2d16da9dd5 Shared versioning 2026-03-28 21:11:08 +01:00
selimaj-dev 9c0f9a1434 Expanded heap size just in case 2026-03-28 21:00:53 +01:00
selimaj-dev d58b0c90fd Small improvements with build 2026-03-28 20:56:40 +01:00
selimaj-dev bf4cec4ea5 Build all 2026-03-28 20:54:05 +01:00
Kr4ight 2d75ee4ee8 Merge pull request #62 from saturnclientmc/zoom-and-freelok-animation
Zoom and freelok animations
2026-03-26 23:57:35 +01:00
selimaj-dev 37569d069d Working freelook 2026-03-26 17:59:33 +01:00
selimaj-dev f44647ba52 Working freelook 2026-03-26 16:56:44 +01:00
selimaj-dev 518c1d9d57 Merge pull request #60 from saturnclientmc/refactor-renderscope
Refactor RenderScope and improved support with 1.21.6
2026-03-26 02:28:33 +01:00
selimaj-dev 7caff20255 Fixed EntityDrawer 2026-03-26 02:27:04 +01:00
selimaj-dev 69698c16f7 Fixed EntityDrawer 2026-03-26 02:16:57 +01:00
selimaj-dev 3705afb593 Saturn Screen 1.21.6 small patches 2026-03-25 23:36:00 +01:00
selimaj-dev ebe7200d65 Improved rounded rectangles #59 2026-03-25 23:26:33 +01:00
selimaj-dev b457f070b9 Improved rounded rectangles #59 2026-03-25 22:18:26 +01:00
selimaj-dev 4f44274111 Fixed rounded corners on RenderScope 1.21.6 #59 2026-03-25 21:46:34 +01:00
selimaj-dev 51c6108e35 Fixed soem stuff on RenderScope 1.21.6 #59 2026-03-25 21:20:14 +01:00
selimaj-dev 3a71cd5b02 Fixed MatrixStack leak on RenderScope 1.21.6 #59 2026-03-25 21:04:53 +01:00
selimaj-dev 9424e2ec71 Refactored RenderScope 1.21.6 #59 2026-03-25 19:24:02 +01:00
selimaj-dev d50d19b609 Refactored RenderScope and MatrixStack for 1.21.5 #59 2026-03-25 19:09:36 +01:00
selimaj-dev bf280d945e Improved #59 2026-03-25 19:00:21 +01:00
selimaj-dev 0b469e6a5a Improved RenderScope for 1.21.4 #59 2026-03-25 18:23:31 +01:00
Kr4ight 57da2f1181 Merge pull request #58 from saturnclientmc/buggy-1.21.6
Buggy 1.21.6
2026-03-25 16:11:48 +01:00
selimaj-dev c771764251 Fixed MatrixStackRef 2026-03-25 16:10:41 +01:00
selimaj-dev a004585cba Matrix3x2fStackRef 2026-03-25 15:51:14 +01:00
selimaj-dev 92a1f70eb1 Fixed DrawContextAccessor 2026-03-25 15:43:18 +01:00
selimaj-dev 1d0c582729 SaturnScreen temp fix 2026-03-25 15:40:56 +01:00
selimaj-dev 0f947b5115 Rendering for 1.21.6 2026-03-25 15:39:41 +01:00
selimaj-dev 8852856d45 Status effect mixin and ShaderUtils for 1.21.6 2026-03-25 14:49:56 +01:00
selimaj-dev f7a882b0ab Remvoed all player animator stuff 2026-03-25 14:35:37 +01:00
selimaj-dev 35b127def2 Copy pasted 1.21.6 2026-03-25 14:34:22 +01:00
selimaj-dev 13a5347412 Fix mixins #57 2026-03-24 14:36:24 +01:00
Kr4ight ca235486bb Merge pull request #56 from saturnclientmc/refactor-mod-system
Refactor mod system #55
2026-03-24 13:10:43 +01:00
selimaj-dev 89f29d5d0e Refactor feature / module system for 1.21.5 #55 2026-03-24 13:09:32 +01:00
selimaj-dev 99929c7d64 Refactor feature / module system for 1.21.5 #55 2026-03-24 13:06:50 +01:00
selimaj-dev 8836cc851b Refactor feature / module system for 1.21.4 #55 2026-03-24 13:01:38 +01:00
selimaj-dev 7849e2e04c Refactored feature / module system #55 2026-03-24 12:05:02 +01:00
Kr4ight 0bf5bf030f Merge pull request #54 from saturnclientmc/mod-preview
Mod Preview #52
2026-03-24 11:30:52 +01:00
selimaj-dev 21cc6936f7 Mod Preview #52 2026-03-24 11:27:13 +01:00
selimaj-dev 5f225953c2 Merge pull request #53 from saturnclientmc/refactor-gif
Refactored gif system
2026-03-23 21:48:54 +01:00
selimaj-dev 9fbeac628c Refactoed 1.21.5 to work with new gif api 2026-03-23 21:05:22 +01:00
selimaj-dev df102fff84 Gif loading 2026-03-23 20:41:58 +01:00
selimaj-dev d3cd883782 Gif loading 2026-03-23 20:33:10 +01:00
selimaj-dev 7ce54c8561 Fixed 1.21.5 end cosmetics shader 2026-03-23 19:31:11 +01:00
selimaj-dev faa18c99ab Fixed hat rotation in 1.21.5 2026-03-23 14:33:26 +01:00
Rudi a73b139256 Merge pull request #50 from RudideC/master
update common
2026-03-21 23:23:48 +01:00
RudideC 02066bf380 Merge branch 'master' of https://github.com/RudideC/saturnclient 2026-03-21 23:23:15 +01:00
RudideC 925cd6d062 Update common 2026-03-21 23:19:41 +01:00
selimaj-dev f45aa843e5 Merge pull request #48 from saturnclientmc/improved-cloak-physics
Improved cloak physics
2026-03-21 22:38:15 +01:00
selimaj-dev 9f4b2f015d Common 2026-03-21 22:36:53 +01:00
selimaj-dev 16b3936649 Improved expressiveness in cloak physics 2026-03-21 22:08:08 +01:00
RudideC 8b8104412c Update common 2026-03-21 21:51:59 +01:00
selimaj-dev 2bbabff979 Fixed the same thing for 1.21.5 2026-03-21 20:46:10 +01:00
selimaj-dev 39dc0963ce Fixed saturn player tracking 2026-03-21 20:40:29 +01:00
Rudi c37efa53b3 Merge pull request #47 from RudideC/master
fixed compilation
2026-03-21 17:59:56 +01:00
RudideC fe629b8bf5 finally works
svg lib wasnt included
2026-03-21 17:59:22 +01:00
RudideC a8e15b98dd now it runs at least 2026-03-21 17:46:17 +01:00
RudideC 1a71f055ff 1.21.5 build changes
same as 1.21.4
2026-03-21 17:36:53 +01:00
RudideC fa68fcad50 1.21.4 build changes
not quite working, close though
2026-03-21 17:36:41 +01:00
Rudi 66e58b31c2 Merge pull request #46 from RudideC/master
1.21.5 support [NOT PRODUCTION READY]
2026-03-21 17:08:13 +01:00
RudideC 938429dca7 idk 2026-03-21 17:06:50 +01:00
RudideC b9eb77f9e1 different import 2026-03-21 17:06:44 +01:00
RudideC a9b331044f prev vs last 2026-03-21 17:06:36 +01:00
RudideC db22e99b0f armor change 2026-03-21 17:06:29 +01:00
RudideC 290da45cde mixin change 2026-03-21 17:06:23 +01:00
RudideC 56e8cde0f9 major change here, hope nothing bad happens 2026-03-21 17:06:17 +01:00
RudideC 6df7aacb56 1.21.5 2026-03-21 17:05:20 +01:00
RudideC c2f19ef3cf updated for 1.21.5 2026-03-21 17:05:09 +01:00
RudideC 6ac91f10ab player anim for 1.21.5 2026-03-21 17:04:58 +01:00
RudideC 52930df0a9 copy pasted 1.21.4 haha 2026-03-21 16:04:22 +01:00
Rudi ac9f765cb6 Change submodule URL to use HTTPS
this works
2026-03-21 15:42:52 +01:00
Kr4ight 4962e0e4c3 Merge pull request #44 from saturnclientmc/cosmetics
Cosmetics
2026-03-21 14:47:33 +01:00
selimaj-dev 020636d293 Cosmetic update 2026-03-21 14:37:11 +01:00
selimaj-dev 0f172ccbab Bucket hat 2026-03-21 13:12:26 +01:00
selimaj-dev 635219a556 bucket hat 2026-03-21 13:12:03 +01:00
selimaj-dev 5e53a2e188 End shader 2026-03-21 10:38:14 +01:00
selimaj-dev 39b4b616b0 Albania mark 2026-03-21 01:54:24 +01:00
selimaj-dev 0471a007ce Improved cloak physics 2026-03-21 00:38:41 +01:00
selimaj-dev b60f218e29 Cloak loading 2026-03-20 23:06:07 +01:00
Kr4ight 67fb65dcf5 Merge pull request #42 from saturnclientmc/feature-implementations
Implemented features and refactored Property
2026-03-20 22:02:39 +01:00
selimaj-dev a6f3fd79b3 Nofog 2026-03-20 21:55:38 +01:00
selimaj-dev ab60119266 Refactored effect 2026-03-20 21:36:18 +01:00
selimaj-dev 6a40d6b6f1 Fixed Dummy for status effects 2026-03-20 21:17:34 +01:00
selimaj-dev 10a4868d8f Fixed config stuff 2026-03-20 21:07:50 +01:00
selimaj-dev 01da8b8198 Improved module implementation structure 2026-03-20 19:42:41 +01:00
selimaj-dev 787ffd7bcb Fixed freelook 2026-03-20 19:22:59 +01:00
selimaj-dev 462b6fc462 Merge pull request #39 from saturnclientmc/1.21-1.21.1-minor-support
1.21 1.21.1 minor support
2026-03-09 17:50:17 -07:00
selimaj-dev 97603f84ec Fixed 2026-03-10 01:48:31 +01:00
selimaj-dev fba84ebb9a Fixed 2026-03-10 01:48:12 +01:00
selimaj-dev edfe1d59c1 1.21.1 (crashing) 2026-03-09 12:27:13 +01:00
selimaj-dev 7b95f083e7 Fixed CloakFeatureRenderer 2026-03-09 12:23:14 +01:00
selimaj-dev 33c8945d42 Fixed CloakFeatureRenderer 2026-03-09 12:07:59 +01:00
selimaj-dev 4e029570d0 Fixed not booting and build.gradle 2026-03-09 11:44:27 +01:00
selimaj-dev e5a418d7b2 Fixed saturn mixins 2026-03-09 11:32:43 +01:00
selimaj-dev b38e247bff Fixed KeyInputHandler 2026-03-09 11:26:27 +01:00
selimaj-dev 59248ee520 Temporarily fixed SaturnScreenFabric 2026-03-09 11:25:52 +01:00
selimaj-dev 1ec0b90f9b RenderScopeImpl fix 2026-03-09 11:25:11 +01:00
selimaj-dev 4dca80b6b1 Temporarily fixed mixins 2026-03-09 11:17:56 +01:00
selimaj-dev 2317d981b1 Temporarily fixed cosmetics 2026-03-09 11:13:35 +01:00
selimaj-dev dbd096dfbf Added 1.21 (copied from 1.21.4) 2026-03-09 10:45:57 +01:00
selimaj-dev edef4f4893 fix 2026-03-09 01:37:45 +01:00
selimaj-dev 9712bc0f47 Merge pull request #37 from saturnclientmc/final-architecture
Final architecture
2026-03-08 17:34:50 -07:00
selimaj-dev e0d0571078 Fix 2026-03-09 01:33:29 +01:00
selimaj-dev c467eb3b95 Improved gradle 2026-03-09 00:31:20 +01:00
selimaj-dev 57f6ffa3d4 Fixed protected 2026-03-08 16:17:13 +01:00
selimaj-dev 2df38b2962 Moved implementations into impl 2026-03-08 16:16:06 +01:00
selimaj-dev f947fa5f17 Fixed a few things 2026-03-08 16:11:00 +01:00
selimaj-dev 9a8c9670c6 Fixed a few things 2026-03-08 16:05:53 +01:00
selimaj-dev 301b75c003 Mixins 2026-03-08 16:00:36 +01:00
selimaj-dev ceeb64282f Modules implementation 2026-03-08 15:55:44 +01:00
selimaj-dev d68e47d1cc Removed old module logic 2026-03-08 13:48:06 +01:00
selimaj-dev b04014a87f Refactor mixins 2026-03-08 13:34:52 +01:00
selimaj-dev 8854726916 Refactor mixins 2026-03-08 13:34:45 +01:00
selimaj-dev f182ac7e91 Refactor interface to feature 2026-03-08 12:50:46 +01:00
selimaj-dev 68a366a30b ServiceClient cloak loading #36 2026-03-08 12:07:34 +01:00
selimaj-dev dfb3c6e614 CloakStore and static loading 2026-03-08 11:58:11 +01:00
selimaj-dev 6af6ecefad Cloaks in common 2026-03-08 11:45:19 +01:00
selimaj-dev 8039862142 Moving cloaks to common 2026-03-08 11:29:45 +01:00
selimaj-dev b6f164b43b ElementRenderer fix 2026-03-08 11:18:21 +01:00
selimaj-dev 962d2c0319 Fabric mod json small change for website 2026-03-08 11:11:09 +01:00
selimaj-dev 12b4a4fc5f Improved separation 2026-03-08 11:05:46 +01:00
selimaj-dev b99840a937 Provider 2026-03-08 02:59:03 +01:00
selimaj-dev f9938a51e9 Fixed font 2026-03-08 02:47:31 +01:00
selimaj-dev 2fa04b079d GLFW 2026-03-08 02:40:23 +01:00
selimaj-dev c22a9ff5ae Implemented text interface 2026-03-08 02:30:47 +01:00
selimaj-dev 66024ebfa2 Implemented all of the refrences 2026-03-08 01:53:22 +01:00
selimaj-dev 66e4a12819 Improved implementation of ref 2026-03-08 01:23:26 +01:00
selimaj-dev 216b1707ae Architecture #35 2026-03-07 23:16:48 +01:00
selimaj-dev bdc21920eb Architecture #35 2026-03-07 23:14:02 +01:00
selimaj-dev 5d2a21dd69 Removed minecraft package bc it's unnecessary 2026-03-07 23:01:56 +01:00
selimaj-dev 7f5acb9daf Updated readme 2026-03-07 21:30:04 +01:00
selimaj-dev 7728c6e818 Common submodule 2026-03-07 21:25:16 +01:00
selimaj-dev ea6d2958e7 Removed common to replace it with a git submodule 2026-03-07 21:22:32 +01:00
selimaj-dev 04a4a1b226 Merge pull request #34 from saturnclientmc/rewritten-with-common-system
Rewritten with common system (Multi version support)
2026-03-07 09:38:50 -08:00
selimaj-dev b59c2ba705 Moved hats and emotes to common 2026-03-07 18:19:18 +01:00
selimaj-dev 4ddd650102 Fixed default font 2026-03-07 18:06:51 +01:00
selimaj-dev 5f63f8b37a Cosmetics fade remove of sidebar 2026-03-07 17:54:19 +01:00
selimaj-dev 9110cbe722 Added all of the mods 2026-03-07 17:49:35 +01:00
selimaj-dev c2144dcfc8 Fixed screen events 2026-03-07 17:43:01 +01:00
selimaj-dev 39d03c4ef6 Fixed skinpreview drawer 2026-03-07 17:30:47 +01:00
selimaj-dev d4107df833 Fixed resize 2026-03-07 17:23:18 +01:00
selimaj-dev 196dd08943 Title menu stuff 2026-03-07 17:19:10 +01:00
selimaj-dev 199635be0b Fixed screen rendering 2026-03-07 17:08:10 +01:00
selimaj-dev d82921976b It runs but.. 2026-03-07 17:04:11 +01:00
selimaj-dev 8bfc3bc699 Refactored 2026-03-07 17:01:49 +01:00
selimaj-dev e348b94720 Refactored 2026-03-07 16:57:31 +01:00
selimaj-dev 35df2b2b27 Client stuff 2026-03-07 16:55:29 +01:00
selimaj-dev a577ee7796 testing 2026-03-07 16:37:59 +01:00
selimaj-dev 8d528387db Screens 2026-03-07 16:13:11 +01:00
selimaj-dev 90db3d3915 fixed saturnclient package 2026-03-07 16:04:35 +01:00
selimaj-dev 4d7a79f517 HudEditor moved to common 2026-03-07 16:00:51 +01:00
selimaj-dev 5b8ce8f048 Idk 2026-03-07 15:49:29 +01:00
selimaj-dev 7fd5493995 Saturn Screen implementation 2026-03-07 15:37:37 +01:00
selimaj-dev 7af945486c SaturnScreen 2026-03-07 02:33:57 +01:00
selimaj-dev 5bd0d249f9 SaturnScreen 2026-03-07 01:01:10 +01:00
selimaj-dev 0cd192107d Working skin preview 2026-03-07 00:57:04 +01:00
selimaj-dev 3aa98d458d Implementing components 2026-03-07 00:49:31 +01:00
selimaj-dev eca1be7d81 Refacted modules in saturnclient 2026-03-07 00:40:58 +01:00
selimaj-dev bdc22e87ae ping, speedometer, and status effects 2026-03-07 00:37:55 +01:00
selimaj-dev 9b73e4d1c9 nofog and tps and zoom 2026-03-07 00:29:57 +01:00
selimaj-dev 439dc2653e nametags fullbright and health display with keystrokes 2026-03-07 00:25:11 +01:00
selimaj-dev 9fee71154e fps and freelook 2026-03-06 23:54:25 +01:00
selimaj-dev afedec45ae day counter crosshair and coordinates 2026-03-06 23:44:19 +01:00
selimaj-dev 5544946769 auto Sprint and Clock 2026-03-06 23:37:03 +01:00
selimaj-dev debf9ce96d Module provider 2026-03-06 23:27:26 +01:00
selimaj-dev c93b0e049d Armor display mod 2026-03-06 23:10:58 +01:00
selimaj-dev 6eb7fa4c8b Transitioned mod helpers to common 2026-03-06 12:45:02 +01:00
selimaj-dev a02653c5ab Working UI 2026-03-06 12:39:49 +01:00
selimaj-dev 12bb846e50 Improved structure 2026-03-06 12:17:31 +01:00
selimaj-dev 58a38ad809 Fixed scroll 2026-03-06 11:58:14 +01:00
selimaj-dev b9657d6f45 Working 2026-03-06 11:50:51 +01:00
selimaj-dev da27d820f7 MatrixStack 2026-03-06 11:45:17 +01:00
selimaj-dev e07ec5a54a ElementRenderer 2026-03-06 11:41:14 +01:00
selimaj-dev 27c5bafb6b Client mixin 2026-03-06 11:37:24 +01:00
selimaj-dev 3291dc1e6e Temporarily fized 1.21.4 2026-03-06 11:28:48 +01:00
selimaj-dev 7ada7ea3c8 Adding more implementations and fixes 2026-03-06 11:10:28 +01:00
selimaj-dev fc87506965 Adding more implementations 2026-03-06 10:59:07 +01:00
selimaj-dev 0084457008 Fixing svgTexture 2026-03-06 10:27:51 +01:00
selimaj-dev d874d5a388 Elements 2026-03-06 10:16:43 +01:00
selimaj-dev 53332157e3 Small change 2026-03-06 01:15:47 +01:00
selimaj-dev e70c1423a4 Fonts 2026-03-06 01:10:51 +01:00
selimaj-dev 9dbf431a55 Element and animations 2026-03-06 00:53:11 +01:00
selimaj-dev 0f41b2786c Improved structure and added RenderScope 2026-03-06 00:29:11 +01:00
selimaj-dev 86d588ffc4 New textures 2026-03-06 00:09:02 +01:00
selimaj-dev c19b92b242 modules with new resources 2026-03-06 00:08:41 +01:00
selimaj-dev 8027b627d8 UI with new resources 2026-03-06 00:08:07 +01:00
selimaj-dev 9c34198138 Fixed assets 2026-03-06 00:02:11 +01:00
selimaj-dev 6226604fa5 Saturn Client provider and jackson 2026-03-05 23:57:30 +01:00
selimaj-dev 98d5bf5ce5 Saturn Client provider 2026-03-05 23:51:28 +01:00
selimaj-dev 9bf04639b3 ImageTexture with abstracted Identifier 2026-03-05 23:46:32 +01:00
selimaj-dev e8671640e3 Using updated config 2026-03-05 23:45:03 +01:00
selimaj-dev daf71b0e8c moved resources 2026-03-05 23:38:56 +01:00
selimaj-dev d2b016ddc7 Resources 2026-03-05 23:37:09 +01:00
selimaj-dev 4bb6cf8489 Config theme and animation config 2026-03-05 23:34:42 +01:00
selimaj-dev cd8a41524f Config manager and property 2026-03-05 23:28:54 +01:00
selimaj-dev 68a43d55b9 Fixed circular dependency 2026-03-05 22:02:12 +01:00
selimaj-dev 8d3f283f4c MinecraftProvider structure 2026-03-05 21:47:58 +01:00
selimaj-dev 2cd1e39ec5 Identifier 2026-03-05 19:44:52 +01:00
selimaj-dev 45ceaf8b39 renamed ui2 to ui 2026-03-05 18:56:57 +01:00
selimaj-dev dc17e9aa44 Ignore build directory 2026-03-05 16:40:34 +01:00
selimaj-dev bd9dc51578 Common sub project 2026-03-05 16:35:48 +01:00
selimaj-dev 1b3c23587d Working 1.21.4 saturn client (but old structure still) 2026-03-05 16:14:18 +01:00
selimaj-dev da24cc584b Added saturnclient 1.21.4 code 2026-03-05 16:07:35 +01:00
selimaj-dev 55b1814c1e Removed unnecessary client side 2026-03-05 16:01:11 +01:00
selimaj-dev 98976ea745 Working common 2026-03-05 15:52:52 +01:00
selimaj-dev e9f0a49389 Working build 2026-03-05 15:41:24 +01:00
selimaj-dev e414bc2097 Fixed gradle issue 2026-03-05 15:34:30 +01:00
selimaj-dev 6711753bec gradle wrapper 2026-03-05 15:32:57 +01:00
selimaj-dev 076c3d67f8 mod Template 2026-03-05 15:31:31 +01:00
selimaj-dev 6af269da11 Common and versions 2026-03-05 15:25:56 +01:00
selimaj-dev b98fe211f0 Starting fresh 2026-03-05 15:24:28 +01:00
selimaj-dev c70ac16978 Merge pull request #31 from saturnclientmc/rewrote-3d-cosmetic-rendering
Rewrote 3d cosmetic rendering #18
2026-03-03 02:39:12 -08:00
selimaj-dev 6ad563fd4a Removed old obj library 2026-03-03 11:32:02 +01:00
selimaj-dev 8be1bab1b6 Rotation 2026-03-03 11:28:34 +01:00
selimaj-dev a553360868 Refactored json 2026-03-03 10:00:58 +01:00
selimaj-dev 944bada2a2 Refactored structure 2026-03-03 09:53:12 +01:00
selimaj-dev 8ed855723b Working cosmetic id mtl loader 2026-03-03 04:10:19 +01:00
selimaj-dev 5b9807c5d3 Mtl loader 2026-03-03 04:05:14 +01:00
selimaj-dev d9e5153452 Halo json 2026-03-03 03:54:05 +01:00
selimaj-dev 92b310e5e2 Working json for model 2026-03-03 03:50:56 +01:00
selimaj-dev 02d973e4e4 Hat model caching 2026-03-03 03:40:44 +01:00
selimaj-dev 2f41fc080c Working custom obj hat 2026-03-03 03:06:03 +01:00
selimaj-dev 80684f7e07 Working mtl obj rendering 2026-03-03 02:51:06 +01:00
selimaj-dev e3ad918755 Working obj rendering 2026-03-03 02:37:53 +01:00
selimaj-dev 178fb1a484 Tesiting 3d obj rendering 2026-03-02 15:01:18 +01:00
selimaj-dev 2b0f1dcb48 Cosmetics refactor 2026-03-02 02:59:09 +01:00
selimaj-dev 154fbd774f Cosmetics refactor to main 2026-03-02 02:56:17 +01:00
selimaj-dev a497d2a14f Cloak sneak and swim handling 2026-03-02 02:50:28 +01:00
selimaj-dev 00af8c4cd7 Improved cloak y velocity handling 2026-03-02 02:11:38 +01:00
selimaj-dev 3a922418ca Fixed pivot 2026-03-02 02:00:30 +01:00
selimaj-dev 96d7f5065c Fixed small waves 2026-03-01 22:17:16 +01:00
selimaj-dev fb85702ee9 Fixed UI animation default settings 2026-02-25 23:55:16 +01:00
selimaj-dev 69cad4caab Updated README 2026-02-25 23:47:37 +01:00
selimaj-dev 23e508138b Screenshot and fixed small animation duration 2026-02-25 18:01:47 +01:00
selimaj-dev 593b9f4760 Merge pull request #30 from saturnclientmc/cloak-physics
Cloak physics #14
2026-02-25 08:51:23 -08:00
selimaj-dev 5b44caa358 Falling physics 2026-02-25 17:47:49 +01:00
selimaj-dev 28d71e63a9 Flat cloak 2026-02-25 17:35:50 +01:00
selimaj-dev 84addc7996 Fixed gaps in curve 2026-02-25 17:11:38 +01:00
selimaj-dev 3f8d68619a Working simulation 2026-02-25 16:52:12 +01:00
selimaj-dev d941334fa3 Fixed gap 2026-02-25 16:34:06 +01:00
selimaj-dev 9c36d33cb2 Working ish system 2026-02-25 16:24:45 +01:00
selimaj-dev 6b03a1441a Fixing the rendering anchoring 2026-02-25 16:06:10 +01:00
selimaj-dev f40d3026e6 Fixed reverse rendering and position 2026-02-25 14:41:22 +01:00
selimaj-dev 2ea122c4a9 Working full cape rendering 2026-02-25 14:17:57 +01:00
selimaj-dev dfce271c37 Cape segment specific rendering 2026-02-25 13:52:07 +01:00
Rudi d40724205c Merge pull request #29 from RudideC/master
bugfixes, changes to animation defaults
2026-02-25 11:05:58 +01:00
RudideC 55afd1e436 changed default font
i think this looks better. thoughts?
2026-02-25 11:03:03 +01:00
RudideC eb3e3d61db changed default animation timings
should be snappier now
2026-02-25 11:02:40 +01:00
RudideC 5da0e3c428 added myself as contributor 2026-02-25 11:02:21 +01:00
RudideC f464d10971 fixed libraries not included 2026-02-25 11:02:08 +01:00
RudideC 6e0bc22c17 bugfix, fixed mod not loading
remap = false makes it so that it works when developing, but not when compiled
2026-02-25 11:01:57 +01:00
selimaj-dev bb95999112 Implementing cape curving 2026-02-25 04:34:57 +01:00
selimaj-dev 9c0f92d034 Fixed title screen 2026-02-25 03:47:05 +01:00
selimaj-dev 9d04958ff2 Merge pull request #28 from saturnclientmc/animation-settings
Animation settings
2026-02-24 18:36:53 -08:00
selimaj-dev cfe0d4a97f Emote wheel and cosmetics animations 2026-02-25 02:07:30 +01:00
selimaj-dev 0995484c09 Emote wheel animation config 2026-02-25 01:28:52 +01:00
selimaj-dev ec2254cf74 Implemented curves for all animations 2026-02-25 01:18:00 +01:00
selimaj-dev 71ae403188 Implementing AnimationConfig directly 2026-02-25 00:54:35 +01:00
selimaj-dev 6689919b3b Fixes and implemented animation curves 2026-02-25 00:39:48 +01:00
selimaj-dev 246e49e521 Animation fix 2026-02-25 00:25:13 +01:00
selimaj-dev eb065b20d7 Animation config 2026-02-25 00:18:16 +01:00
Rudi 038c859133 Merge pull request #27 from RudideC/master
0.0.7-beta1
2026-02-24 21:41:47 +01:00
RudideC 434afa7f62 0.0.7-beta1
all code integrated, beta release is ready
2026-02-24 21:38:40 +01:00
RudideC fd492b91a0 animation fine-tuning, easing change
can change between ease-in-out and ease-out (default)
2026-02-24 21:34:56 +01:00
RudideC 48a41af3b0 changed timings
personal preference, i think these should be a setting
2026-02-24 21:27:46 +01:00
RudideC 6da9eb55ba fixed bug, where modmenu wouldn't have scroll 2026-02-24 21:27:23 +01:00
RudideC 22efd8b6e0 moved to svg
yay
2026-02-24 16:23:34 +01:00
Rudi b71d779900 Merge pull request #25 from RudideC/master
Nametags, Ping, Clock, Tps
2026-02-24 10:05:52 +01:00
Rudi 6c3dbd90a0 Merge branch 'master' into master 2026-02-24 10:05:15 +01:00
RudideC 1b16098121 0.0.7-alpha1
will do some testing and then move towards the beta release
2026-02-24 09:41:50 +01:00
RudideC 092fcfb009 new icons
wow
2026-02-24 09:40:06 +01:00
RudideC 008a0927b6 clock mod 2026-02-24 09:37:37 +01:00
RudideC 3c2f75e438 tps mod
missing the image, will add later
2026-02-24 09:21:23 +01:00
RudideC 09c354337d rename 2026-02-24 09:19:17 +01:00
selimaj-dev dc6387cc27 Merge pull request #24 from saturnclientmc/services-and-ui-tweaks
Services and UI tweaks
2026-02-23 12:32:03 -08:00
selimaj-dev bff18a05d1 Changed ip to the actual server 2026-02-23 21:30:50 +01:00
selimaj-dev 6717d7e901 Fixed UUID and cloak 2026-02-23 18:51:28 +01:00
selimaj-dev bbf4b17494 Attempting to fix sendplayer 2026-02-23 17:46:28 +01:00
selimaj-dev 68b4bfc439 Working peer player 2026-02-23 16:29:48 +01:00
RudideC 77f5350496 forgot to add icon 2026-02-23 11:38:57 +01:00
RudideC 3c1914c30a added ping mod
wow
2026-02-23 11:27:34 +01:00
RudideC ae8ec4b4ef fixed modmenu scroll 2026-02-23 11:27:25 +01:00
RudideC bdd421e1da nametag module finished
yay
2026-02-23 10:20:41 +01:00
RudideC 7b480d1b8b unit
wow
2026-02-23 09:52:42 +01:00
selimaj-dev 2a425335f0 Working emote streaming 2026-02-23 09:34:38 +01:00
RudideC 02f8b486f3 obfuscate names, name color 2026-02-23 09:24:27 +01:00
selimaj-dev f0966c86f3 Fixing emote system 2026-02-23 09:20:10 +01:00
RudideC 65c6c2a284 colors, can hide heart emoji 2026-02-23 09:11:06 +01:00
RudideC 9fe05722f9 player, hostile, passive filter 2026-02-23 08:47:02 +01:00
selimaj-dev 61be7c317f Fixed the hat rendering issue 2026-02-23 07:22:44 +01:00
selimaj-dev df35497af2 Trying to find the hat rendering issue 2026-02-23 07:18:54 +01:00
selimaj-dev 8614e1bdd1 Working service system 2026-02-23 06:47:08 +01:00
selimaj-dev c5c38247cc Improved build.gradle 2026-02-23 03:45:26 +01:00
selimaj-dev f9c0d3fe58 Fixed keystrokes 2026-02-23 00:24:14 +01:00
selimaj-dev 44c0b69c4d Fixed keystrokes 2026-02-22 23:59:25 +01:00
selimaj-dev 0cfa7b81ef SVG mod icons 2026-02-22 23:40:39 +01:00
selimaj-dev 9caf47d81b Fixed svg issues 2026-02-22 18:11:58 +01:00
selimaj-dev 3c27d3b377 Working svg renderer 2026-02-22 16:41:37 +01:00
selimaj-dev 66b96102f8 Working svg renderer 2026-02-22 16:41:26 +01:00
selimaj-dev 7fc05df240 Svg image rendering 2026-02-22 14:35:57 +01:00
selimaj-dev d32f124974 Removed todos 2026-02-22 04:27:30 +01:00
selimaj-dev fe4d999643 Merge pull request #23 from saturnclientmc/new-ui-theme-and-animations
New UI theme and animations
2026-02-21 18:48:24 -08:00
selimaj-dev cd84866ee8 Optional staggering 2026-02-22 03:44:22 +01:00
selimaj-dev cd5cac7f69 Shift menu animations 2026-02-22 03:36:39 +01:00
selimaj-dev 377031c0a9 Fixed click bugs 2026-02-22 03:20:16 +01:00
selimaj-dev 85d4a1de38 Animated mods 2026-02-22 02:44:58 +01:00
selimaj-dev 44d7aa3641 Hoo lee sheet 2026-02-22 02:31:56 +01:00
selimaj-dev 53732c5df3 Animation stagger and a few slide fixes 2026-02-22 02:19:04 +01:00
selimaj-dev 265b44187b Animation stagger delay 2026-02-22 01:38:30 +01:00
selimaj-dev f46f9c474e Animation Stagger element 2026-02-22 01:34:54 +01:00
selimaj-dev 049c92785d Fixed bugs and restructured roles 2026-02-22 01:25:22 +01:00
selimaj-dev 37ec729d1d Blurring 2026-02-22 01:11:12 +01:00
selimaj-dev 2747ad366e Fixed title menu freezing 2026-02-22 01:06:33 +01:00
selimaj-dev 7de3d5c9bc Fixed keybinding crashing when invalid 2026-02-22 00:20:12 +01:00
selimaj-dev f952fa7095 Better animation system 2026-02-22 00:12:13 +01:00
selimaj-dev e62da29a49 Sub namespace loading 2026-02-21 23:11:22 +01:00
selimaj-dev d3258edbc7 Cleaner Config 2026-02-21 22:55:47 +01:00
selimaj-dev 2096925a02 Smaller HexInput 2026-02-21 22:42:21 +01:00
selimaj-dev 3db44bab85 Fixed HexInput and stuff 2026-02-21 22:37:56 +01:00
selimaj-dev bba12146ea HexInput 2026-02-21 22:17:59 +01:00
selimaj-dev 4403a31c93 Fix splashoverlay and saturnmodule 2026-02-21 20:59:07 +01:00
selimaj-dev 6ee2fc3b0e Improved theme usability 2026-02-21 20:42:04 +01:00
selimaj-dev 3b0ab53d86 Fixed Theme issue 2026-02-21 20:35:52 +01:00
selimaj-dev 7683e7b91e Removed NamedProperty 2026-02-21 20:18:52 +01:00
selimaj-dev 3ba6202b43 Improved scroll 2026-02-21 20:09:19 +01:00
selimaj-dev a49212ca64 Attempting to fix scroll 2026-02-21 20:02:12 +01:00
selimaj-dev 88fe53b907 Fixed sidebar with abstractions 2026-02-21 19:57:40 +01:00
selimaj-dev 52f80281bd Fixed a few issues 2026-02-21 19:46:40 +01:00
selimaj-dev 2f93822996 Scrollbar 2026-02-21 19:38:19 +01:00
selimaj-dev 8d8cbc8b28 Implemenented theme 2026-02-21 19:29:36 +01:00
selimaj-dev a7c5f0d3df Fixed font stuff in screens 2026-02-21 19:19:54 +01:00
selimaj-dev 60d83530ae Tabmenu text and texture button 2026-02-21 19:18:42 +01:00
selimaj-dev 569aecde4a Scroll 2026-02-21 18:51:58 +01:00
selimaj-dev 9fd79aadd5 Notification 2026-02-21 18:47:19 +01:00
selimaj-dev e031672d72 Clicakble texture 2026-02-21 18:46:03 +01:00
selimaj-dev 289a2cd78a Button 2026-02-21 18:44:24 +01:00
selimaj-dev aab4f02dc0 Sidebar 2026-02-21 18:40:14 +01:00
selimaj-dev 5b37131bfd Saturn module 2026-02-21 18:34:12 +01:00
selimaj-dev 79ceaf67a1 Cosmetic Preview 2026-02-21 18:28:30 +01:00
selimaj-dev b0dbdeafa3 Toggle 2026-02-21 18:25:17 +01:00
selimaj-dev 3bc21a5b30 Keybinding selector and select 2026-02-21 18:19:44 +01:00
selimaj-dev 953182e56a Theme font, and implemented input 2026-02-21 18:13:36 +01:00
selimaj-dev 39cea14183 Theme padding and radius 2026-02-21 18:10:57 +01:00
selimaj-dev 8baf28fcfe Implemented theme 2026-02-21 18:05:26 +01:00
selimaj-dev 619c4bce21 Fixed role 2026-02-20 21:02:00 +01:00
selimaj-dev cc80fc5239 Merge pull request #21 from saturnclientmc/refactored-services-and-splash
Refactored services and splash
2026-02-20 11:56:48 -08:00
selimaj-dev f9039fdf67 Splash Background 2026-02-20 20:53:27 +01:00
selimaj-dev bb93d72f66 Progress bar 2026-02-20 20:12:17 +01:00
selimaj-dev 86be2eb956 Tested auth and fixed title menu 2026-02-20 19:44:43 +01:00
selimaj-dev eb40903372 SetScreen mixin 2026-02-20 18:11:58 +01:00
selimaj-dev 039d2cd032 SetScreen mixin 2026-02-20 17:35:59 +01:00
selimaj-dev 365063e8d5 Custom splash overlay 2026-02-20 16:41:09 +01:00
RudideC b43b076ca0 bugfix
you can now activate hitboxes without opening emotes
2026-02-20 11:10:00 +01:00
RudideC 71a7721c52 entity types 2026-02-20 10:12:19 +01:00
RudideC 2bcad83409 added nametags logo 2026-02-20 09:34:55 +01:00
selimaj-dev 6750abcc11 Service client refactor finalized 2026-02-20 03:47:32 +01:00
selimaj-dev 46d3408be9 TabList 2026-02-20 03:46:51 +01:00
selimaj-dev 8015a23342 Emote service 2026-02-20 03:42:56 +01:00
selimaj-dev 4e6bdce019 Buy cosmetics service 2026-02-20 03:41:17 +01:00
selimaj-dev 68519ac00c UI service auth refactored 2026-02-20 03:38:05 +01:00
selimaj-dev 50da953dc4 Added myself as the owner again 2026-02-20 03:25:33 +01:00
selimaj-dev f5776450f0 Name tag service 2026-02-20 03:24:09 +01:00
selimaj-dev 3b3283c6b0 Hat service 2026-02-20 03:11:08 +01:00
selimaj-dev 3f06efe8f4 Hat feature renderer 2026-02-20 03:09:50 +01:00
selimaj-dev 2d021e886f Cloaks 2026-02-20 03:07:31 +01:00
selimaj-dev 96f201cdb4 Auth 2026-02-20 02:45:42 +01:00
selimaj-dev 57c1d21407 Updated session version 2026-02-20 02:15:15 +01:00
selimaj-dev 5d8068c964 Before switching session versions 2026-02-20 01:44:03 +01:00
selimaj-dev 418c47f483 New structure 2026-02-20 00:31:19 +01:00
selimaj-dev 08c001afbb Session build 2026-02-19 23:21:03 +01:00
RudideC 052a2ac06c fixed small issue 2026-02-19 13:33:54 +01:00
RudideC 38041b01cf nametags module 2026-02-19 13:22:30 +01:00
Rudi fdda25464b Merge pull request #17 from RudideC/master
added github actions
2026-02-18 12:41:34 +01:00
RudideC 6bfdf66cdd push
will this work?
2026-02-18 12:37:03 +01:00
RudideC 82f8d0a550 maybe this will work?? 2026-02-18 12:21:17 +01:00
RudideC 81bfe41e25 no sources file 2026-02-18 12:06:58 +01:00
RudideC db0e627f5c fixed bug, dont upload sources jar 2026-02-18 10:13:04 +01:00
RudideC da970354e8 fix deprecation, simplified 2026-02-18 10:02:29 +01:00
RudideC 045e5cd5da test 2026-02-18 09:36:58 +01:00
RudideC e517377bdd ts 2026-02-18 09:25:09 +01:00
RudideC 520b5221b5 new version 2026-02-18 09:17:34 +01:00
selimaj-dev c567b282a5 Merge pull request #12 from RudideC/master
added health display
2026-02-13 12:03:56 +01:00
RudideC 5f19abebbf added health display
ts tuff
2026-02-13 10:06:13 +01:00
Klesti Selimaj 58eda56f37 Merge pull request #11 from RudideC/master
zoom mod
2026-01-21 21:57:42 +01:00
RudideC 50cab402d4 image 2026-01-21 21:52:28 +01:00
RudideC fa7161d371 zoom module 2026-01-21 21:52:24 +01:00
Klesti Selimaj 252808a467 Merge pull request #10 from RudideC/master
added nofog, daycounter and speedometer
2026-01-20 21:56:36 +01:00
RudideC 31e9a8d56e speedometer module finished
logo included
2026-01-20 15:30:57 +01:00
RudideC ba1b68d8fa speedometer works in vehicles 2026-01-20 14:57:59 +01:00
RudideC dcd82fa58e default is x axis 2026-01-20 14:51:22 +01:00
RudideC 4132a05c8f blocks/tick -> blocks/s 2026-01-20 14:44:29 +01:00
RudideC 47f8dd5e22 testing speedometer 2026-01-20 14:35:49 +01:00
RudideC 9b30ec7016 small change 2026-01-20 14:18:44 +01:00
RudideC d10f229ffa daycounter image 2026-01-19 20:55:07 +01:00
RudideC 061ed674e3 daycounter mod
counts days
2026-01-19 20:54:59 +01:00
RudideC 5b5f781095 nofog mod
you can customise if you want to be able to see through liquids, but only lava and powdered snow work (not water)
2026-01-17 00:24:39 +01:00
Klesti Selimaj 48f7aefbf6 Merge pull request #9 from RudideC/master 2026-01-16 22:12:15 +01:00
RudideC 8f111badf6 oops, forgot 2026-01-16 22:05:35 +01:00
RudideC 687bda68a1 renamed potiondisplay to status effects
this is the name most clients use
2026-01-16 22:04:57 +01:00
RudideC 8e2c986ae4 fullbright
new mod
2026-01-16 22:02:48 +01:00
RudideC faa95e664a faster timeout
(so i don't have to wait minutes before testing)
2026-01-16 22:02:25 +01:00
RudideC c9230da168 faster builds
yes
2026-01-16 22:01:59 +01:00
RudideC 7002fbec0e prevent race condition 2026-01-16 19:41:37 +01:00
RudideC 496d27b861 enabled mod list updates
each mod enable/disable updates the list
2026-01-16 19:41:29 +01:00
RudideC 3c37b80bc2 dont sprint when using item
anticheat compliance
2026-01-16 19:02:51 +01:00
289 changed files with 6547 additions and 19374 deletions
-12
View File
@@ -1,12 +0,0 @@
<factorypath>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/io.github.llamalad7/mixinextras-fabric/0.4.1/8d1a9e96afb990367fa1f904d17580d164da72e3/mixinextras-fabric-0.4.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-analysis/9.7.1/f97a3b319f0ed6a8cd944dc79060d3912a28985f/asm-analysis-9.7.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/net.fabricmc/fabric-mixin-compile-extensions/0.6.0/6fda0c85c419f0a259da017a1424f27507eb30f0/fabric-mixin-compile-extensions-0.6.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/net.fabricmc/mapping-io/0.2.1/2a1aeedce104191bbb9e39e26abfbfeaf9b0590b/mapping-io-0.2.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-tree/9.7.1/3a53139787663b139de76b627fca0084ab60d32c/asm-tree-9.7.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/fabric-loom/1.21.4/net.fabricmc.yarn.1_21_4.1.21.4+build.7-v2/mappings.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/net.fabricmc/sponge-mixin/0.15.4+mixin.0.8.7/6a12aacc794f1078458433116e9ed42c1cc98096/sponge-mixin-0.15.4+mixin.0.8.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/9.7.1/f0ed132a49244b042cd0e15702ab9f2ce3cc8436/asm-9.7.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-util/9.7.1/9e23359b598ec6b74b23e53110dd5c577adf2243/asm-util-9.7.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-commons/9.7.1/406c6a2225cfe1819f102a161e54cc16a5c24f75/asm-commons-9.7.1.jar" enabled="true" runInBatchMode="false"/>
</factorypath>
+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)));
+38
View File
@@ -0,0 +1,38 @@
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.
# 1.21.x builds with Java 21 and 26.x with Java 25. The last version listed is the default
# (Gradle runs on it), and the Gradle commands below point toolchain detection at both.
- name: Set up Java 25 and 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
25
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 \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_21_X64,JAVA_HOME_25_X64
+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
+106
View File
@@ -0,0 +1,106 @@
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.
# 1.21.x builds with Java 21 and 26.x with Java 25. The last version listed is the default
# (Gradle runs on it), and the Gradle commands below point toolchain detection at both.
- name: Set up Java 25 and 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
25
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 \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_21_X64,JAVA_HOME_25_X64
- 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
+3
View File
@@ -0,0 +1,3 @@
[submodule "common"]
path = common
url = https://git.selimaj.dev/saturnclientmc/common.git
+1 -1
View File
@@ -1,6 +1,6 @@
# Licenses
This project is under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0), but a lot of code taken from other open source projects that are under other licenses that need to be mentioned due to legal reasons.
This project is under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0), but a lot of code taken from other open source projects that are under other licenses that need to be mentioned due to legal or ethical reasons.
| File | License |
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
+75 -8
View File
@@ -1,19 +1,86 @@
<p align="center">
<img src="src/main/resources/assets/saturnclient/icon.png" alt="OSUI" width="150px"/>
<img src="logo.png" alt="Saturn Client Logo" width="150px"/>
</p>
<h1 align="center">Saturn Client</h1>
<p align="center">
<b>Saturn Client is a free and open source Minecraft client</b>
<b>A free and open-source Minecraft client.</b><br/>
Built with performance, customization, and community in mind.
</p>
![image](https://github.com/user-attachments/assets/b0b23e72-c2eb-49aa-b49c-4d3cc4d60fcc)
<p align="center">
<img src="screenshot.png" alt="Saturn Client Screenshot"/>
</p>
# Credits
## About
- Cloak rendering: `IIpho3nix` & `Dhyan Blum`
- Who Am I: `FokshaWasTaken`
- Panorama: https://www.planetminecraft.com/texture-pack/minecraft-cavesand-cliffs-part-ii-main-menu-panorama/
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.
If any of the code or assets are in your name and you want to remove them, please submit an issue, we are still in alpha to have any artists so we would also appreciate if you could help us make some cosmetics
Saturn runs on Fabric and supports **Minecraft 1.21.4 to 1.21.11 and 26.1 to 26.3**, with one build per version (the 26.1 build also runs on 26.1.1 and 26.1.2). If something doesn't work on your version, please open an issue.
## Features
- 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
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. The 26.x versions compile with Java 25, which Gradle downloads when it isn't installed.
## Contributing
We welcome contributions of all kinds:
- Code improvements
- Bug fixes
- UI enhancements
- 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.
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
- **Cloak Rendering:** `IIpho3nix` & Saturn
- **Who Am I:** `FokshaWasTaken`
- **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.
We aim to properly credit all contributors and respect original creators of their respective art.
## License
Saturn Client is free and open-source under the **Apache-2.0** license.
<p align="center">
Made with ❤️ by the Saturn Client team
</p>
+82
View File
@@ -0,0 +1,82 @@
# Version guide
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, and 26.1 to 26.3) 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, 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 `loomx.loom_version` in the root `gradle.properties`.
Minecraft is obfuscated up to 1.21.11 and ships with Mojang's names from 26.1. The `loom-back-compat` plugin (`loomx`) applies the matching Loom for each version: the remapping one for 1.21.x, whose jar is `remapJar`'s output, and the plain one for 26.x, whose jar is `jar`'s. 26.x also compiles with Java 25 (21 before), set per version in `build.gradle.kts`; Gradle downloads a missing JDK through the foojay resolver, and CI installs both. An optional `minecraft_compat` in a version's `gradle.properties` widens the Minecraft versions its jar loads on, as 26.1's does for its hotfixes.
## Mappings
Every version is built with Mojang's official names (`loom.officialMojangMappings()`), the names Minecraft itself ships from 26.1 on. Yarn isn't used any more: 1.21.11 has the last Yarn release.
Mojang renames things between versions. `src/` uses the active version's (1.21.11's) names, and `stonecutter.gradle.kts` lists replacements that swap in each version's name when Stonecutter processes it: for example `Identifier`, which was `ResourceLocation` before 1.21.11, `AvatarRenderState`, which was `PlayerRenderState` before 1.21.9, and `GuiGraphics`, which is `GuiGraphicsExtractor` from 26.1. Add a replacement there when a rename touches many lines, and only for a name that appears nowhere else in `src/`: replacements work both ways, so the new name is also turned back into the old one for older versions (a `mulPose`/`rotate` replacement would rename `MatrixStackRef.rotate` too). From 26.2 the open screen and overlay live on `Gui` rather than `Minecraft`, so code goes through `impl.ui.Screens` for them.
From 26.3 Minecraft uses SDL instead of GLFW, with SDL's numbers for keys (scancodes), modifiers and mouse buttons. Saturn keeps GLFW's numbers, which `common` uses and the config's keybinds store, and `impl.provider.Keys` converts at the boundary: key events and mouse buttons coming in, and key codes going to the game. Use `InputConstants`' key constants rather than GLFW's in version-specific code.
26.3 also compiles every shader through SPIR-V, so shader inputs and outputs need an explicit `layout(location = …)`. Saturn's `glass.fsh` is shared by every version from 1.21.6 and switches its layout with the defines `GlassRenderer` sets, as it already did for `HAS_LINE_WIDTH`. A class that only moved package needs just a version comment on its import, and a renamed method a version comment where it's called.
Released 1.21.x jars run on intermediary names (`method_1234`), not Mojang's, and Loom renames everything that refers to Minecraft when it builds them. So a mixin can't implement one of `common`'s Ref interfaces by relying on a vanilla method that happens to have the same name: the vanilla method is renamed in the jar, and the interface method is left without an implementation (`AbstractMethodError`). Shadow the vanilla method and implement the Ref method explicitly, as `WindowMixin` does. For the same reason, Ref methods mustn't share a name and parameters with a method of the class they're mixed into, or the mixin replaces the vanilla method in development.
## 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 submit(PoseStack matrices, SubmitNodeCollector output, int light, AvatarRenderState state, float limbAngle, float limbDistance) {
//?} else
//public void render(PoseStack matrices, MultiBufferSource output, int light, AvatarRenderState 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.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.rendertype.RenderSetup;
//?} else {
/*import net.minecraft.client.renderer.RenderStateShard.MultiTextureStateShard;
*///?}
```
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. Version blocks can nest (Stonecutter writes an inactive block inside another as `/^ … ^/`, as in `GlassRenderer`), but for more than two ranges of the same thing, prefer `//?} else if <condition> {`, 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`, `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 (or when its class first loads, such as in a world), 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.
-89
View File
@@ -1,89 +0,0 @@
plugins {
id 'fabric-loom' version '1.9-SNAPSHOT'
id 'maven-publish'
}
version = project.mod_version
group = project.maven_group
repositories {}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
// Switch back to Yarn
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modApi "dev.felnull:special-model-loader:1.4.0+pre.1"
include "dev.felnull:special-model-loader:1.4.0+pre.1"
include modImplementation("dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}")
//You might want bendy-lib. playerAnimator will wrap it.
// include modRuntimeOnly("io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}")
}
repositories {
maven { url "https://maven.parchmentmc.org" }
maven {
name = "FelNull Maven"
url = "https://maven.felnull.dev"
}
maven {
name "KosmX's maven"
url 'https://maven.kosmx.dev/'
}
}
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
}
}
def targetJavaVersion = 21
tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8"
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
it.options.release.set(targetJavaVersion)
}
}
java {
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
if (JavaVersion.current() < javaVersion) {
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
}
archivesBaseName = project.archives_base_name
withSourcesJar()
}
jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"}
}
from("NOTICE") {
rename { "${it}_${project.archivesBaseName}"}
}
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
repositories {}
}
+84
View File
@@ -0,0 +1,84 @@
// 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("dev.kikugie.loom-back-compat")
`maven-publish`
}
val minecraftVersion = property("minecraft_version") as String
// The Minecraft versions the jar loads on (fabric.mod.json); a range where hotfixes share a jar.
val minecraftCompat = findProperty("minecraft_compat") as String? ?: minecraftVersion
val javaVersion = if (sc.current.parsed >= "26.1") 25 else 21
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")
loomx.applyMojangMappings()
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 minecraftCompat,
"loader_version" to loaderVersion,
"java" to "JAVA_$javaVersion",
)
inputs.properties(values)
filteringCharset = "UTF-8"
filesMatching(listOf("fabric.mod.json", "*.mixins.json")) { expand(values) }
}
tasks.withType<JavaCompile>().configureEach {
options.release = javaVersion
}
java {
// Loom attaches the sources jar to remapSourcesJar and build.
withSourcesJar()
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
}
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"])
}
}
}
Submodule
+1
Submodule common added at e8f686b2d3
File diff suppressed because one or more lines are too long
+12 -10
View File
@@ -1,13 +1,15 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
org.gradle.jvmargs=-Xmx3G
org.gradle.parallel=true
# Reuse task outputs (e.g. compiled classes) when inputs haven't changed.
org.gradle.caching=true
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.7
loader_version=0.16.10
fabric_version=0.114.0+1.21.4
# 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.0.5-beta2
maven_group = org.saturnclient
archives_base_name = saturnclient
mod_version=0.1.5-beta
player_anim = 2.0.3+1.21.4
# Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes)
emotes_version=0.1.2
# Loom, for every Minecraft version (read by loom-back-compat).
loomx.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-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+5 -8
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -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/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+52 -34
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,36 +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
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@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%

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

-9
View File
@@ -1,9 +0,0 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
gradlePluginPortal()
}
}
+35
View File
@@ -0,0 +1,35 @@
pluginManagement {
repositories {
maven("https://maven.fabricmc.net/")
maven("https://maven.kikugie.dev/releases")
gradlePluginPortal()
mavenCentral()
}
}
plugins {
// Downloads the Java toolchain a version needs (Java 25 for 26.1+) when it isn't installed.
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
id("dev.kikugie.stonecutter") version "0.9.8"
// Picks the Loom variant per version: remapping for 1.21.x, plain for the unobfuscated 26.1+.
// Its version is loomx.loom_version in gradle.properties.
id("dev.kikugie.loom-back-compat") version "0.4.2"
}
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, Fabric Loader 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",
"26.1", "26.2", "26.3",
)
stonecutter {
create(rootProject) {
versions(stonecutterVersions)
vcsVersion = "1.21.11"
}
}
@@ -0,0 +1,548 @@
package org.saturnclient.impl.cosmetics;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
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.emotes.core.Animation;
import org.saturnclient.emotes.core.Bone;
import org.saturnclient.emotes.core.bend.LimbBend;
import org.saturnclient.emotes.core.bend.Vec3;
import org.saturnclient.emotes.fabric.access.BendablePart;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import org.saturnclient.saturnclient.SaturnRenderState;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.renderer.texture.OverlayTexture;
import com.mojang.blaze3d.vertex.VertexConsumer;
//? if >=1.21.9 {
import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else
//import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.model.EquipmentClientInfo;
import net.minecraft.client.resources.model.EquipmentAssetManager;
import net.minecraft.client.resources.model.EquipmentClientInfo.LayerType;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.renderer.entity.RenderLayerParent;
//? if >=1.21.11 {
import net.minecraft.client.model.player.PlayerModel;
//?} else
//import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.equipment.Equippable;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.equipment.EquipmentAsset;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.Mth;
/**
* 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 RenderLayer<AvatarRenderState, PlayerModel> {
private final EquipmentAssetManager 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(RenderLayerParent<AvatarRenderState, PlayerModel> context,
EquipmentAssetManager equipmentModelLoader) {
super(context);
this.equipmentModelLoader = equipmentModelLoader;
Arrays.fill(segmentValues, 0.0f);
}
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentClientInfo.LayerType layerType) {
Equippable equippableComponent = (Equippable) stack
.get(DataComponents.EQUIPPABLE);
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
EquipmentClientInfo equipmentModel = this.equipmentModelLoader
.get((ResourceKey<EquipmentAsset>) equippableComponent.assetId().get());
return !equipmentModel.getLayers(layerType).isEmpty();
} else {
return false;
}
}
private void vertex(VertexConsumer vc, Matrix4f posMat, Matrix3f normalMat,
net.minecraft.world.phys.Vec3 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.addVertex(
p.x(), p.y(), p.z(),
color,
u, v,
overlay, light,
n.x(), n.y(), n.z());
}
private void renderCapeQuad(VertexConsumer vertexConsumer, PoseStack.Pose entry,
net.minecraft.world.phys.Vec3 bottomLeft, net.minecraft.world.phys.Vec3 bottomRight, net.minecraft.world.phys.Vec3 topRight, net.minecraft.world.phys.Vec3 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.pose();
Matrix3f normalMat = entry.normal();
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(PoseStack.Pose entry, VertexConsumer vertexConsumer,
AvatarRenderState 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
// =============================
net.minecraft.world.phys.Vec3[][] inner = new net.minecraft.world.phys.Vec3[H_PARTS + 1][PARTS + 1];
net.minecraft.world.phys.Vec3[][] outer = new net.minecraft.world.phys.Vec3[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 net.minecraft.world.phys.Vec3(xPos, y - curveY, z - curveZ);
outer[x][i] = new net.minecraft.world.phys.Vec3(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);
net.minecraft.world.phys.Vec3 innerTL = inner[x + 1][i];
net.minecraft.world.phys.Vec3 innerTR = inner[x][i];
net.minecraft.world.phys.Vec3 innerBL = inner[x + 1][i + 1];
net.minecraft.world.phys.Vec3 innerBR = inner[x][i + 1];
net.minecraft.world.phys.Vec3 outerTL = outer[x + 1][i];
net.minecraft.world.phys.Vec3 outerTR = outer[x][i];
net.minecraft.world.phys.Vec3 outerBL = outer[x + 1][i + 1];
net.minecraft.world.phys.Vec3 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++) {
net.minecraft.world.phys.Vec3 innerTop = inner[H_PARTS][i];
net.minecraft.world.phys.Vec3 innerBot = inner[H_PARTS][i + 1];
net.minecraft.world.phys.Vec3 outerTop = outer[H_PARTS][i];
net.minecraft.world.phys.Vec3 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++) {
net.minecraft.world.phys.Vec3 innerTop = inner[0][i];
net.minecraft.world.phys.Vec3 innerBot = inner[0][i + 1];
net.minecraft.world.phys.Vec3 outerTop = outer[0][i];
net.minecraft.world.phys.Vec3 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++) {
net.minecraft.world.phys.Vec3 outerL = outer[x][0];
net.minecraft.world.phys.Vec3 outerR = outer[x + 1][0];
net.minecraft.world.phys.Vec3 innerR = inner[x + 1][0];
net.minecraft.world.phys.Vec3 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++) {
net.minecraft.world.phys.Vec3 innerL = inner[x][PARTS];
net.minecraft.world.phys.Vec3 innerR = inner[x + 1][PARTS];
net.minecraft.world.phys.Vec3 outerR = outer[x + 1][PARTS];
net.minecraft.world.phys.Vec3 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);
}
}
/** A torso bend past this curves the back more than a flat cloak can follow. */
private static final float MAX_CLOAK_BEND = (float) Math.toRadians(10);
/** Whether each animation bends the torso past that at any point. */
private static final Map<Animation, Boolean> BENDS_TORSO = Collections.synchronizedMap(new WeakHashMap<>());
/**
* Whether the player is playing an emote that curves the torso (a twerk, the griddy). The cloak
* would cut through the arched back, so it's hidden for the whole emote rather than only while
* the back is curved, which would flicker.
*/
private static boolean playingTorsoBend(SaturnPlayer player) {
return org.saturnclient.emotes.fabric.Emotes.playing(player.uuid)
.map(playing -> BENDS_TORSO.computeIfAbsent(playing.animation(), CloakFeatureRenderer::bendsTorso))
.orElse(false);
}
private static boolean bendsTorso(Animation animation) {
for (float tick = 0; tick <= animation.length(); tick += 0.5f) {
if (animation.sample(tick).get(Bone.BODY).filter(body -> Math.abs(body.bend()) > MAX_CLOAK_BEND).isPresent()) {
return true;
}
}
return false;
}
/**
* An emote that bends the torso a little leans the chest from the hips, so the cloak hangs from
* where the top of the back ends up, turned as much as the torso is there.
*/
private static void followBend(PoseStack matrices, ModelPart body) {
LimbBend bend = ((BendablePart) (Object) body).saturnEmotes$getBend();
if (bend == null || bend.isStraight()) {
return;
}
// The top of the torso, in the body's own pixels
float[] top = new float[3];
bend.bendPoint(0, 0, 0, top);
matrices.translate(top[0] / 16.0f, top[1] / 16.0f, top[2] / 16.0f);
Vec3 axis = bend.axis();
Quaternionf rotation = new Quaternionf().rotationAxis(bend.angleAt(0), axis.x(), axis.y(), axis.z());
//? if >=26.3 {
/*matrices.rotate(rotation);
*///?} else
matrices.mulPose(rotation);
}
// 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
//? if >=1.21.9 {
public void submit(PoseStack matrices,
//?} else
//public void render(PoseStack matrices,
//? if >=1.21.9 {
SubmitNodeCollector output,
//?} else
//MultiBufferSource output,
int light, AvatarRenderState state, float limbAngle, float limbDistance) {
if (state.isInvisible || !state.showCape
//? if >=1.21.9 {
|| state.skin.cape() != null) {
//?} else
//|| state.skin.capeTexture() != null) {
return;
}
//? if >=1.21.8 {
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
//?} else
//SaturnPlayer player = SaturnPlayer.get(state.name);
if (player == null || playingTorsoBend(player)) {
return;
}
IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak);
if (customCape == null
|| this.hasCustomModelForLayer(state.chestEquipment, 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.pushPose();
// On the back: the whole player (which emotes like the backflip turn), then the body's position
// and turn (which crouching and emotes move), then the chest's lean when an emote bends the torso
PlayerModel model = this.getParentModel();
//? if >=1.21.5 {
model.root().translateAndRotate(matrices);
model.body.translateAndRotate(matrices);
//?} else {
/*model.root().translateAndRotate(matrices);
model.body.translateAndRotate(matrices);
*///?}
followBend(matrices, model.body);
matrices.translate(0.0f, 0.0f, 0.12f);
if (this.hasCustomModelForLayer(state.chestEquipment, LayerType.HUMANOID)) {
matrices.translate(0.0F, -0.053125F, 0.06875F);
}
long now = System.currentTimeMillis();
if (now - lastUpdate >= 20) {
float velX = Math.min(0.8f, state.capeLean / 108.0f);
float rawVelY = state.capeFlap;
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
float value = Math.max(0.0f, Math.min(2.0f, state.isVisuallySwimming ? 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 = Mth.clamp(state.capeLean / 150.0f, -1.0f, 1.0f);
//? if >=1.21.9 {
output.submitCustomGeometry(matrices, ShaderUtils.getRenderLayer(customCape), (entry, vertexConsumer) -> {
renderCape(entry, vertexConsumer, state, l, OverlayTexture.NO_OVERLAY);
});
//?} else
//renderCape(matrices.last(), output.getBuffer(ShaderUtils.getRenderLayer(customCape)), state, l, OverlayTexture.NO_OVERLAY);
matrices.popPose();
}
}
@@ -0,0 +1,65 @@
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 org.saturnclient.saturnclient.SaturnRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
//? if >=1.21.9 {
import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else
//import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.renderer.entity.RenderLayerParent;
//? if >=1.21.11 {
import net.minecraft.client.model.player.PlayerModel;
//?} else
//import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import com.mojang.blaze3d.vertex.PoseStack;
public class HatFeatureRenderer extends RenderLayer<AvatarRenderState, PlayerModel> {
public HatFeatureRenderer(RenderLayerParent<AvatarRenderState, PlayerModel> context) {
super(context);
}
@Override
//? if >=1.21.9 {
public void submit(PoseStack matrices,
//?} else
//public void render(PoseStack matrices,
//? if >=1.21.9 {
SubmitNodeCollector output,
//?} else
//MultiBufferSource output,
int light, AvatarRenderState state, float limbAngle, float limbDistance) {
//? if >=1.21.8 {
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
//?} else
//SaturnPlayer player = SaturnPlayer.get(state.name);
if (state.isInvisible || player == null || player.hat.isEmpty()) {
return;
}
matrices.pushPose();
// On the head, as vanilla places head items: the whole player (which emotes like the backflip
// turn), then the head's position, turn and scale (which crouching and emotes move)
PlayerModel model = this.getParentModel();
//? if >=1.21.5 {
model.root().translateAndRotate(matrices);
model.head.translateAndRotate(matrices);
//?} else {
/*model.root().translateAndRotate(matrices);
model.head.translateAndRotate(matrices);
*///?}
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, output, light,
OverlayTexture.NO_OVERLAY);
matrices.popPose();
}
}
@@ -0,0 +1,56 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.javagl.obj.*;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.Identifier;
public class MtlLoader {
static Map<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>();
static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
try (InputStream is = Minecraft.getInstance().getResourceManager()
.getResource(mtlId).get().open()) {
List<Mtl> mtlList = MtlReader.read(is);
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
for (Mtl mtl : mtlList) {
mtlMap.put(mtl.getName(), mtl);
}
return mtlMap;
}
}
public static Map<String, Mtl> of(Identifier mtlId) {
if (loadedMtls.containsKey(mtlId))
return loadedMtls.get(mtlId);
try {
Map<String, Mtl> mtl = loadMtl(mtlId);
loadedMtls.put(mtlId, mtl);
return mtl;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static Map<String, Mtl> cosmetic(String cosmeticId) {
int index = cosmeticId.indexOf('_');
String mtlId = (index == -1) ? "" : cosmeticId.substring(index + 1);
if (mtlId.isEmpty())
mtlId = "white";
return of(Identifier.fromNamespaceAndPath("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl"));
}
}
@@ -0,0 +1,118 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.javagl.obj.Mtl;
import de.javagl.obj.Obj;
import net.minecraft.client.Minecraft;
//? if >=1.21.9 {
import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else
//import net.minecraft.client.renderer.MultiBufferSource;
import com.mojang.blaze3d.vertex.PoseStack;
import org.joml.Quaternionf;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.resources.Identifier;
public class ObjModel {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static final Map<Identifier, ObjModel> loadedObjModels = new HashMap<>();
public record Config(
@JsonProperty("translate") Vec3d transform,
@JsonProperty("scale") Vec3d scale,
@JsonProperty("rotation") Vec3d rotation) {
public Config {
transform = transform == null ? new Vec3d(0, 0, 0) : transform;
scale = scale == null ? new Vec3d(1, 1, 1) : scale;
rotation = rotation == null ? new Vec3d(0, 0, 0) : rotation;
}
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
public record Vec3d(float x, float y, float z) {
}
}
Obj obj;
Config config;
public static ObjModel cosmetic(String cosmeticKind, String id) {
return of(Identifier.fromNamespaceAndPath("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model"));
}
public static ObjModel of(Identifier obj) {
if (loadedObjModels.containsKey(obj))
return loadedObjModels.get(obj);
ObjModel o = new ObjModel(obj);
loadedObjModels.put(obj, o);
return o;
}
ObjModel(Identifier identifier) {
Identifier objId = Identifier.fromNamespaceAndPath(identifier.getNamespace(), identifier.getPath() + ".obj");
Identifier jsonId = Identifier.fromNamespaceAndPath(identifier.getNamespace(), identifier.getPath() + ".json");
try {
this.obj = ObjRenderer.loadObj(objId);
} catch (IOException e) {
e.printStackTrace();
}
// Try loading JSON config if it exists
ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
Optional<Resource> resource = resourceManager.getResource(jsonId);
if (resource.isPresent()) {
try (InputStream stream = resource.get().open()) {
this.config = MAPPER.readValue(stream, Config.class);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void render(Map<String, Mtl> mtlMap, PoseStack matrices,
//? if >=1.21.9 {
SubmitNodeCollector output,
//?} else
//MultiBufferSource output,
int light, int overlay) {
matrices.pushPose();
rotate(matrices, com.mojang.math.Axis.XP.rotationDegrees(180.0f));
rotate(matrices, com.mojang.math.Axis.YP.rotationDegrees(180.0f));
if (this.config != null) {
matrices.translate(config.transform.x, config.transform.y, config.transform.z);
matrices.scale(config.scale.x, config.scale.y, config.scale.z);
rotate(matrices, com.mojang.math.Axis.XP.rotationDegrees(config.rotation.x));
rotate(matrices, com.mojang.math.Axis.YP.rotationDegrees(config.rotation.y));
rotate(matrices, com.mojang.math.Axis.ZP.rotationDegrees(config.rotation.z));
}
ObjRenderer.renderObj(this.obj, mtlMap, matrices, output, light, overlay);
matrices.popPose();
}
// 26.3 renamed PoseStack's quaternion mulPose to rotate.
private static void rotate(PoseStack matrices, Quaternionf rotation) {
//? if >=26.3 {
/*matrices.rotate(rotation);
*///?} else
matrices.mulPose(rotation);
}
}
@@ -0,0 +1,131 @@
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.Minecraft;
//? if >=1.21.11 {
import net.minecraft.client.renderer.rendertype.RenderType;
//?} else
//import net.minecraft.client.renderer.RenderType;
import com.mojang.blaze3d.vertex.VertexConsumer;
//? if >=1.21.9 {
import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else
//import net.minecraft.client.renderer.MultiBufferSource;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.resources.Identifier;
public class ObjRenderer {
public static Obj loadObj(Identifier objId) throws IOException {
try (InputStream is = Minecraft.getInstance().getResourceManager()
.getResource(objId).get().open()) {
return ObjReader.read(is);
}
}
public static void renderObj(Obj obj,
Map<String, Mtl> mtlMap,
PoseStack matrices,
//? if >=1.21.9 {
SubmitNodeCollector output,
//?} else
//MultiBufferSource output,
int light,
int overlay) {
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;
IdentifierRef texture = IdentifierRef.ofVanilla("textures/misc/white.png");
if (mtl != null) {
String mapKd = mtl.getMapKd();
if (mapKd != null && !mapKd.startsWith("#")) {
texture = IdentifierRef.of(mapKd);
}
}
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();
}
}
RenderType layer = ShaderUtils.getRenderLayer(texture);
final float fr = r, fg = g, fb = b;
final Obj finalObj = groupObj;
//? if >=1.21.9 {
output.submitCustomGeometry(matrices, layer,
(entry, consumer) -> drawGroup(finalObj, consumer, entry, light, overlay, fr, fg, fb));
//?} else
//drawGroup(finalObj, output.getBuffer(layer), matrices.last(), light, overlay, fr, fg, fb);
}
}
private static void drawGroup(Obj obj, VertexConsumer consumer, PoseStack.Pose 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);
}
}
}
private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
PoseStack.Pose 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.addVertex(matrix, x, y, z)
.setColor(r, g, b, 1f)
.setUv(u, v)
.setOverlay(overlay)
.setLight(light)
.setNormal(matrix, nx, ny, nz);
}
}
@@ -0,0 +1,74 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.saturnclient.SaturnClient;
import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.resources.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.setPixel(x, y, pixels[index++]);
}
}
} else {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setPixel(x, y, bufferedImage.getRGB(x, y));
}
}
}
SaturnClient.client.execute(() -> {
//? if >=1.21.5 {
DynamicTexture texture = new DynamicTexture(
() -> identifier.toString(),
nativeImage);
//?} else
//DynamicTexture texture = new DynamicTexture(nativeImage);
SaturnClient.client.getTextureManager().register(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));
}
}
@@ -0,0 +1,124 @@
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.renderer.RenderPipelines;
//? if >=1.21.11 {
import net.minecraft.client.renderer.rendertype.RenderType;
//?} else
//import net.minecraft.client.renderer.RenderType;
//? if <1.21.5 {
/*import net.minecraft.client.renderer.RenderStateShard;
import com.mojang.blaze3d.vertex.VertexFormat.Mode;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
*///?}
//? if >=1.21.11 {
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.rendertype.RenderSetup;
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
//?} else {
/*import net.minecraft.client.renderer.RenderStateShard.MultiTextureStateShard;
import net.minecraft.client.renderer.blockentity.TheEndPortalRenderer;
*///?}
import net.minecraft.resources.Identifier;
//? if >=26.3
//import com.mojang.renderpearl.api.pipeline.ColorTargetState;
//? if >=26.1 {
/*import java.util.HashMap;
import java.util.Map;
*///?}
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.END_PORTAL_SNIPPET })
.withLocation("pipeline/end_gateway")
.withShaderDefine("PORTAL_LAYERS", 16)
.withCull(false)
.build());
//?}
//? if >=1.21.11 {
public static final RenderType END_GATEWAY = RenderType.create("end_gateway",
RenderSetup.builder(END_GATEWAY_PIPELINE)
.withTexture("Sampler0", AbstractEndPortalRenderer.END_SKY_LOCATION)
.withTexture("Sampler1", AbstractEndPortalRenderer.END_PORTAL_LOCATION).createRenderSetup());
//?} else if >=1.21.6 {
/*public static final RenderType END_GATEWAY = RenderType.create("end_gateway", 1536, false, false,
END_GATEWAY_PIPELINE,
RenderType.CompositeState.builder()
.setTextureState(MultiTextureStateShard.builder().add(TheEndPortalRenderer.END_SKY_LOCATION, false)
.add(TheEndPortalRenderer.END_PORTAL_LOCATION, false).build())
.createCompositeState(false));
*///?} else if >=1.21.5 {
/*public static final RenderType END_GATEWAY = RenderType.create("end_gateway", 1536, false, false,
END_GATEWAY_PIPELINE,
RenderType.CompositeState.builder()
.setTextureState(MultiTextureStateShard.builder().add(TheEndPortalRenderer.END_SKY_LOCATION, false, false)
.add(TheEndPortalRenderer.END_PORTAL_LOCATION, false, false).build())
.createCompositeState(false));
*///?} else {
/*public static final RenderType END_GATEWAY = RenderType.create(
"end_gateway",
DefaultVertexFormat.POSITION,
Mode.QUADS,
1536,
false,
false,
RenderType.CompositeState.builder()
.setShaderState(RenderType.RENDERTYPE_END_GATEWAY_SHADER)
.setTextureState(MultiTextureStateShard.builder()
.add(TheEndPortalRenderer.END_SKY_LOCATION, false, false)
.add(TheEndPortalRenderer.END_PORTAL_LOCATION, false, false)
.build())
.setCullState(RenderStateShard.NO_CULL)
.createCompositeState(false));
*///?}
public static RenderType 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);
}
}
// Cosmetics are drawn unlit, without the lightmap or the directional shading, as the dragon's
// alpha pipeline drew them before 26.1. 26.1 dropped that pipeline, so this is the entity shader
// with its lighting and overlay turned off.
//? if >=26.1 {
/*public static final RenderPipeline UNLIT_CUTOUT_PIPELINE = RenderPipelines.register(
RenderPipeline.builder(new RenderPipeline.Snippet[] {
RenderPipelines.ENTITY_EMISSIVE_SNIPPET })
.withLocation(Identifier.fromNamespaceAndPath("saturnclient", "pipeline/unlit_cutout"))
.withShaderDefine("ALPHA_CUTOUT", 0.1f)
.withShaderDefine("NO_CARDINAL_LIGHTING")
.withShaderDefine("NO_OVERLAY")
.withCull(false)
// From 26.3 the snippet leaves the color target to each pipeline.
//? if >=26.3
//.withColorTargetState(ColorTargetState.DEFAULT)
.build());
private static final Map<Identifier, RenderType> UNLIT_CUTOUT = new HashMap<>();
*///?}
private static RenderType entityAlpha(Identifier texture) {
//? if >=26.1 {
/*return UNLIT_CUTOUT.computeIfAbsent(texture, id -> RenderType.create("saturn_unlit_cutout",
RenderSetup.builder(UNLIT_CUTOUT_PIPELINE).withTexture("Sampler0", id).createRenderSetup()));
*///?} else if >=1.21.11 {
return RenderTypes.dragonExplosionAlpha(texture);
//?} else
//return RenderType.dragonExplosionAlpha(texture);
}
}
@@ -0,0 +1,11 @@
package org.saturnclient.impl.features.entity;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -0,0 +1,78 @@
package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.entity.LivingEntity;
/**
* Fabric implementation of {@link EntityFeature}.
*
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* 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 {
private final Minecraft mc;
public EntityFeatureImpl(Minecraft mc) {
this.mc = mc;
}
@Override
public boolean isTargetingLivingEntity() {
HitResult target = mc.hitResult;
if (target == null || target.getType() != HitResult.Type.ENTITY)
return false;
Entity hit = ((EntityHitResult) target).getEntity();
return hit instanceof LivingEntity;
}
// ---------------------------------------------------------------
// Render-state adapter
// ---------------------------------------------------------------
/**
* Wraps a {@link HealthRenderState} (already injected into the
* Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}.
*
* This is constructed in LivingEntityRendererMixin, where both the
* render-state mixin interface and the entity name are available.
*/
public static final class RenderStateEntityState implements EntityState {
private final String customName;
private final HealthRenderState hrs;
public RenderStateEntityState(String customName, HealthRenderState hrs) {
this.customName = customName;
this.hrs = hrs;
}
@Override
public String getCustomName() {
return customName;
}
@Override
public float getHealth() {
return hrs.saturn$getHealth();
}
@Override
public float getMaxHealth() {
return hrs.saturn$getMaxHealth();
}
@Override
public EntityType getEntityType() {
return hrs.saturn$getEntityType();
}
}
}
@@ -0,0 +1,24 @@
package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature;
/**
* Injected interface on
* {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
*
* Carries the entity's health snapshot and classification into the render
* render state, so NametagsMod's replacement nametag (applied in
* LivingEntityRendererMixin) can use them without touching the live entity again.
*/
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityFeature.EntityType saturn$getEntityType();
void saturn$setEntityType(EntityFeature.EntityType type);
}
@@ -0,0 +1,81 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.impl.features.render.ZoomFov;
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;
//? if >=26.1
//import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.Camera;
import net.minecraft.world.entity.Entity;
//? if >=1.21.11 {
import net.minecraft.world.level.Level;
//?} else
//import net.minecraft.world.level.BlockGetter;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
// Freelook keeps the camera's rotation after vanilla points it along the entity's view. From 26.1
// that happens in alignWithEntity, which takes the camera's own entity.
//? if >=26.1 {
/*@Shadow
public abstract Entity entity();
@Inject(method = "alignWithEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(float tickDelta, CallbackInfo ci) {
saturn$lockRotation(entity());
}
// The camera works out the FOV from 26.1 (GameRenderer.getFov before).
@Inject(method = "calculateFov", at = @At("RETURN"), cancellable = true)
private void onCalculateFov(float tickDelta, CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(ZoomFov.apply(cir.getReturnValue()));
}
*///?} else if >=1.21.11 {
@Inject(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(Level focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing,
float tickDelta, CallbackInfo ci) {
saturn$lockRotation(cameraEntity);
}
//?} else {
/*@Inject(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockGetter focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing,
float tickDelta, CallbackInfo ci) {
saturn$lockRotation(cameraEntity);
}
*///?}
@Unique
private void saturn$lockRotation(Entity cameraEntity) {
if (FreelookMod.isFreeLooking() && cameraEntity instanceof LocalPlayer) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && SaturnClient.client.player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getXRot());
cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYRot());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(),
cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!FreelookMod.isFreeLooking() && cameraEntity instanceof LocalPlayer) {
firstTime = true;
}
}
}
@@ -0,0 +1,62 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.mod.mods.FreelookMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.util.Mth;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "turn", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
// noinspection ConstantValue// IntelliJ is incorrect here, this code block is
// reachable
if (FreelookMod.isFreeLooking() && (Object) this instanceof LocalPlayer) {
double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15);
this.cameraPitch = Mth.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta;
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return this.cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return this.cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float pitch) {
this.cameraPitch = pitch;
}
@Override
@Unique
public void freelook$setCameraYaw(float yaw) {
this.cameraYaw = yaw;
}
}
@@ -0,0 +1,24 @@
package org.saturnclient.impl.features.mixins.render;
//? if >=1.21.6 {
import net.minecraft.client.renderer.fog.FogRenderer;
//?} else
//import net.minecraft.client.renderer.FogRenderer;
import org.saturnclient.mod.mods.NoFogMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(FogRenderer.class)
public class BackgroundRendererMixin {
// fog
@ModifyVariable(method = "setupFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFogMod.isActive()) {
return viewDistance * 2.0f;
}
return viewDistance;
}
}
@@ -0,0 +1,65 @@
package org.saturnclient.impl.features.mixins.render;
//? if >=1.21.6
import net.minecraft.client.Minecraft;
//? if >=1.21.6
import net.minecraft.client.Options;
import net.minecraft.client.renderer.GameRenderer;
//? if >=1.21.6
import org.saturnclient.impl.ui.SaturnScreenFabric;
//? if >=1.21.6
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.impl.features.render.ZoomFov;
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;
/**
* Overrides the game's field-of-view when {@link org.saturnclient.mod.mods.ZoomMod} is active.
*
* The old code checked both {@code Zoom.isZooming} and
* {@code Zoom.shouldZoom()} separately. The refactored
* {@link org.saturnclient.mod.mods.ZoomMod#shouldZoom()} already ORs both conditions
* ({@code enabled.value && isZooming}), so only one check is needed.
*
* No {@link org.saturnclient.common.module.ModProvider} is required
* here because zooming is a pure render-time override with no per-tick
* engine queries.
*/
@Mixin(GameRenderer.class)
public class GameRendererMixin {
/**
* After the vanilla FOV has been computed, divide it by the zoom
* level to produce the narrowed field-of-view.
*/
// From 26.1 the camera works out the FOV, so CameraMixin applies the zoom there.
//? if <26.1 {
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(ZoomFov.apply(cir.getReturnValue()));
}
//?}
//? 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.
// From 26.1 the setting is read while extracting the options' render state.
//? if >=26.1
//@Redirect(method = "extractOptions", at = @At(value = "INVOKE",
//? if <26.1
@Redirect(method = "render", at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/Options;getMenuBackgroundBlurriness()I"))
private int useSaturnBlur(Options options) {
if (Screens.current() instanceof SaturnScreenFabric saturnScreen) {
return saturnScreen.screen.getBackgroundBlur();
}
return options.getMenuBackgroundBlurriness();
}
//?}
}
@@ -0,0 +1,45 @@
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;
//? if >=26.1 {
/*import net.minecraft.client.renderer.LightmapRenderStateExtractor;
*///?} else
import net.minecraft.client.renderer.LightTexture;
import org.saturnclient.mod.mods.FullbrightMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Overrides the lightmap's gamma computation when
* {@link FullbrightMod} is active, replacing the vanilla
* brightness with a user-configured value.
*
* The old import was {@code org.saturnclient.feature.features.Fullbright};
* it is now {@link FullbrightMod}. The injection target and
* strategy are unchanged.
*/
// From 26.1 the gamma is worked out while extracting the lightmap's render state.
//? if >=26.1 {
/*@Mixin(LightmapRenderStateExtractor.class)
*///?} else
@Mixin(LightTexture.class)
public class LightmapTextureManagerMixin {
/**
* If fullbright is active, substitute the feature's brightness
* value for whatever vanilla calculated.
*/
//? if >=26.1 {
/*@ModifyExpressionValue(method = "extract", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
*///?} else
@ModifyExpressionValue(method = "updateLightTexture", 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;
}
}
@@ -0,0 +1,59 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Implements {@link HealthRenderState} on
* {@link LivingEntityRenderState}, injecting health and entity-type
* storage into every living entity's render-state snapshot.
*
* Previously imported {@code NametagsInterface.EntityType}; the enum
* now lives in {@link EntityFeature.EntityType} which is the single
* canonical definition shared by the mixin layer and the feature layer.
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read there right away for
* NametagsMod's replacement nametag.
*/
@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;
}
}
@@ -0,0 +1,62 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.entity.AgeableMob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.network.chat.Component;
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}, and applies {@link NametagsMod}'s replacement nametag to the state's
* display name.
*
* 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> {
@Inject(method = "extractRenderState", 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 Player)
type = EntityFeature.EntityType.PLAYER;
else if (entity instanceof Monster)
type = EntityFeature.EntityType.HOSTILE;
else if (entity instanceof AgeableMob)
type = EntityFeature.EntityType.PASSIVE;
else
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.nameTag != null) {
String replacement = NametagsMod.getNametagString(
new EntityFeatureImpl.RenderStateEntityState(state.nameTag.getString(), hrs));
if (replacement != null) {
state.nameTag = Component.literal(replacement);
}
}
}
}
@@ -0,0 +1,93 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
//? if >=26.2 {
/*import net.minecraft.client.gui.Hud;
*///?} else
import net.minecraft.client.gui.Gui;
import net.minecraft.client.DeltaTracker;
import org.saturnclient.impl.ui.RenderScopeImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.screens.HudEditor;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Drives per-frame rendering for all registered features.
*
* This mixin is intentionally kept thin: it creates a {@link RenderScope},
* iterates the enabled feature list from {@link ModManager}, and
* dispatches either {@link HudMod#renderHud} (for HUD elements) or
* {@link Mod#render} (for world-overlay features like Crosshair).
*
* No feature-specific logic lives here — all decisions about what to
* draw are made inside each feature class.
*
* Changes from the original:
* - Package imports updated to the refactored feature names (no
* more {@code feature.features.*}).
* - The HUD-editor screen check still uses the existing
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
* unchanged infrastructure.
*/
// The HUD is drawn by Hud from 26.2 (by Gui before).
//? if >=26.2 {
/*@Mixin(Hud.class)
*///?} else
@Mixin(Gui.class)
public class RenderMixin {
//? if >=26.1 {
/*@Inject(method = "extractChat", at = @At("TAIL"))
*///?} else
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(GuiGraphics context, DeltaTracker tickCounter, CallbackInfo ci) {
Font textRenderer = SaturnClient.client.font;
// 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 (Screens.current() instanceof SaturnScreenFabric f
&& f.screen instanceof HudEditor)
return;
RenderScope scope = new RenderScopeImpl(context);
for (Mod feature : ModManager.ENABLED_MODS) {
if (!feature.isEnabled())
continue;
if (feature instanceof HudMod hud) {
ModLayout dim = hud.getDimensions();
scope.getMatrixStack().push();
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value);
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value);
if (dim.renderBackground) {
scope.drawRoundedRectangle(0, 0, dim.width, dim.height,
dim.radius.value, dim.bgColor.value);
}
hud.renderHud(scope);
scope.getMatrixStack().pop();
} else {
// Non-HUD features (e.g. CrosshairFeature) use the plain render hook.
feature.render(scope);
}
}
}
}
@@ -0,0 +1,30 @@
package org.saturnclient.impl.features.mixins.world;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.protocol.game.ClientboundSetTimePacket;
import org.saturnclient.mod.mods.TpsMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Feeds world-time packets into {@link TpsMod}'s TPS estimator.
*
* This is the only place in the entire codebase that reads a network
* packet and passes data to a feature. The feature's
* {@link TpsMod#onTimePacket(long)} method handles all averaging
* logic; the mixin is intentionally reduced to a single forwarding call.
*
* The only change from the original is renaming the import from
* {@code feature.features.Tps} to {@link TpsMod}.
*/
@Mixin(ClientPacketListener.class)
public class TpsMixin {
@Inject(method = "handleSetTime", at = @At("HEAD"))
private void onWorldTimeUpdate(ClientboundSetTimePacket packet, CallbackInfo ci) {
TpsMod.onTimePacket(packet.gameTime());
}
}
@@ -0,0 +1,37 @@
package org.saturnclient.impl.features.network;
import org.saturnclient.common.feature.NetworkFeature;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.PlayerInfo;
/**
* Fabric implementation of {@link NetworkFeature}.
*
* Connection presence is checked via the network handler.
* Ping is read from the player's {@link PlayerListEntry} which the
* server populates; returns {@code -1} when unavailable.
*/
public class NetworkFeatureImpl implements NetworkFeature {
private final Minecraft mc;
public NetworkFeatureImpl(Minecraft mc) {
this.mc = mc;
}
@Override
public boolean hasNetwork() {
return mc.getConnection() != null;
}
@Override
public int getPing() {
if (mc.player == null || mc.getConnection() == null)
return -1;
PlayerInfo entry = mc.getConnection().getPlayerInfo(mc.player.getUUID());
return entry != null ? entry.getLatency() : -1;
}
}
@@ -0,0 +1,266 @@
package org.saturnclient.impl.features.player;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.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 Minecraft mc;
public PlayerFeatureImpl(Minecraft mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Internal helper — never returns null
// ---------------------------------------------------------------
/** Returns the local player, or {@code null} if one is not loaded. */
private LocalPlayer 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.keyUp.isDown();
}
@Override
public boolean isBackPressed() {
return mc.options.keyDown.isDown();
}
@Override
public boolean isLeftPressed() {
return mc.options.keyLeft.isDown();
}
@Override
public boolean isRightPressed() {
return mc.options.keyRight.isDown();
}
@Override
public boolean isJumpPressed() {
return mc.options.keyJump.isDown();
}
@Override
public boolean isAttackPressed() {
return mc.options.keyAttack.isDown();
}
@Override
public boolean isUsePressed() {
return mc.options.keyUse.isDown();
}
@Override
public boolean isSneaking() {
return hasPlayer() && player().isShiftKeyDown();
}
@Override
public boolean isUsingItem() {
return hasPlayer() && player().isUsingItem();
}
@Override
public boolean hasHorizontalCollision() {
return hasPlayer() && player().horizontalCollision;
}
@Override
public boolean isOnGround() {
return hasPlayer() && player().onGround();
}
@Override
public void setSprinting(boolean sprinting) {
if (hasPlayer())
player().setSprinting(sprinting);
}
// ---------------------------------------------------------------
// Velocity
// ---------------------------------------------------------------
@Override
public Velocity getVelocity() {
if (!hasPlayer())
return Velocity.ZERO;
var v = player().getDeltaMovement();
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().getMainHandItem() : ItemStack.EMPTY);
}
@Override
public ItemStackRef getHelmet() {
//? if >=1.21.5 {
return wrap(hasPlayer() ? player().getItemBySlot(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().getItemBySlot(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().getItemBySlot(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().getItemBySlot(EquipmentSlot.FEET) : ItemStack.EMPTY);
//?} else
//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().getActiveEffectsMap().values();
List<EffectRef> result = new ArrayList<>(effects.size());
for (MobEffectInstance instance : effects) {
result.add((EffectRef) instance);
}
return result;
}
@Override
public List<? extends EffectRef> getDummyEffects() {
return List.of(
(EffectRef) new MobEffectInstance(
BuiltInRegistries.MOB_EFFECT.get(Identifier.withDefaultNamespace("speed")).get(), 12000, 2),
(EffectRef) new MobEffectInstance(
BuiltInRegistries.MOB_EFFECT.get(Identifier.withDefaultNamespace("strength")).get(), 12000, 2),
(EffectRef) new MobEffectInstance(
BuiltInRegistries.MOB_EFFECT.get(Identifier.withDefaultNamespace("fire_resistance")).get(), 12000, 2));
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static ItemStackRef wrap(ItemStack stack) {
return (ItemStackRef) (Object) stack;
}
}
@@ -0,0 +1,64 @@
package org.saturnclient.impl.features.render;
import org.saturnclient.common.feature.RenderFeature;
import net.minecraft.client.Minecraft;
import net.minecraft.client.CameraType;
/**
* Fabric implementation of {@link RenderFeature}.
*
* Window dimensions and FPS are read directly from
* {@link MinecraftClient}. Camera perspective control delegates to
* {@link net.minecraft.client.option.GameOptions}.
*/
public class RenderFeatureImpl implements RenderFeature {
private final Minecraft mc;
public RenderFeatureImpl(Minecraft mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Window
// ---------------------------------------------------------------
@Override
public int getScaledWindowWidth() {
return mc.getWindow().getGuiScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return mc.getWindow().getGuiScaledHeight();
}
// ---------------------------------------------------------------
// Performance
// ---------------------------------------------------------------
@Override
public int getFps() {
return mc.getFps();
}
// ---------------------------------------------------------------
// Camera perspective
// ---------------------------------------------------------------
@Override
public boolean isFirstPerson() {
return mc.options.getCameraType() == CameraType.FIRST_PERSON;
}
@Override
public void setFirstPerson() {
mc.options.setCameraType(CameraType.FIRST_PERSON);
}
@Override
public void setThirdPersonBack() {
mc.options.setCameraType(CameraType.THIRD_PERSON_BACK);
}
}
@@ -0,0 +1,19 @@
package org.saturnclient.impl.features.render;
import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.mod.mods.ZoomMod;
/** The zoom mods' change to the field of view, applied where each version computes it. */
public final class ZoomFov {
private ZoomFov() {
}
public static float apply(float fov) {
if (ZoomMod.shouldZoom()) {
return fov / ZoomMod.getZoomLevel();
} else if (FreelookMod.shouldZoomOut()) {
return fov / FreelookMod.getZoomLevel();
}
return fov;
}
}
@@ -0,0 +1,37 @@
package org.saturnclient.impl.features.world;
import org.saturnclient.common.feature.WorldFeature;
import net.minecraft.client.Minecraft;
/**
* 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 Minecraft mc;
public WorldFeatureImpl(Minecraft mc) {
this.mc = mc;
}
@Override
public boolean hasWorld() {
return mc.level != 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() {
// World.getTime() was a shortcut for this, and 1.21.11 removed it.
return hasWorld() ? mc.level.getLevelData().getGameTime() : 0L;
}
}
@@ -0,0 +1,13 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.resources.Identifier;
@Mixin(Identifier.class)
public abstract class IdentifierMixin implements IdentifierRef {
@Shadow
public abstract String toString();
}
@@ -0,0 +1,35 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.world.item.ItemStack;
@Mixin(ItemStack.class)
public abstract class ItemStackMixin implements ItemStackRef {
@Shadow
public abstract boolean isEmpty();
@Shadow
public abstract int getMaxDamage();
@Shadow
public abstract int getDamageValue();
// ItemStackRef's methods delegate to ItemStack's, whose names production renames.
@Override
public boolean isEmptyStack() {
return isEmpty();
}
@Override
public int getMaxItemDamage() {
return getMaxDamage();
}
@Override
public int getItemDamage() {
return getDamageValue();
}
}
@@ -0,0 +1,79 @@
package org.saturnclient.impl.mixins;
import com.mojang.blaze3d.vertex.PoseStack;
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 com.mojang.math.Axis;
*///?}
/**
* 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(PoseStack.class)
//? if <1.21.6 {
/*public abstract class MatrixStackMixin implements MatrixStackRef {
@Shadow
public abstract void pushPose();
@Shadow
public abstract void popPose();
@Shadow
public abstract void translate(float x, float y, float z);
@Shadow
public abstract void scale(float x, float y, float z);
@Shadow
//? if >=1.21.5
public abstract void mulPose(Quaternionfc quaternion);
//? if <1.21.5
//public abstract void mulPose(Quaternionf quaternion);
@Shadow
//? if >=1.21.5
public abstract void rotateAround(Quaternionfc quaternion, float originX, float originY, float originZ);
//? if <1.21.5
//public abstract void rotateAround(Quaternionf quaternion, float originX, float originY, float originZ);
// MatrixStackRef's methods delegate to PoseStack's, whose names production renames.
@Override
public void push() {
pushPose();
}
@Override
public void pop() {
popPose();
}
@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) {
rotateAround(Axis.ZN.rotationDegrees(angle), originX, originY, originZ);
}
@Override
public void rotate(float angle) {
mulPose(Axis.ZN.rotationDegrees(angle));
}
}
*///?} else
public abstract class MatrixStackMixin {}
@@ -0,0 +1,174 @@
package org.saturnclient.impl.mixins;
import java.io.File;
import java.io.InputStream;
import java.util.UUID;
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.impl.ui.Screens;
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.Minecraft;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.client.gui.screens.options.OptionsScreen;
import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
import net.minecraft.client.Options;
import net.minecraft.client.User;
import com.mojang.blaze3d.platform.Window;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraft.resources.Identifier;
@Mixin(Minecraft.class)
public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Shadow
public File gameDirectory;
@Shadow
private Window window;
@Shadow
private ReloadableResourceManager resourceManager;
@Shadow
public Options options;
@Shadow
private User user;
@Shadow
public abstract void stop();
@Override
public void scheduleStop() {
stop();
}
@Override
public File getRunDirectory() {
return gameDirectory;
}
@Override
public InputStream getResource(IdentifierRef identifier) {
try {
return resourceManager.getResource((Identifier) (Object) identifier).get().open();
} catch (Exception e) {
return null;
}
}
@Override
public WindowRef getWindow() {
return (WindowRef) (Object) this.window;
}
@Override
public void setScreen(SaturnScreen screen) {
Screens.set(new SaturnScreenFabric(screen));
}
@Override
public void setScreen(MinecraftScreen screen) {
SaturnScreenFabric exitTarget = new SaturnScreenFabric(new TitleMenu());
switch (screen) {
case SelectWorld:
Screens.set(new SelectWorldScreen(exitTarget));
break;
case Multiplayer:
Screens.set(new JoinMultiplayerScreen(exitTarget));
break;
case Options:
// Only 26.1 and 26.2 take whether a world is open.
//? if >=26.3 {
/*Screens.set(new OptionsScreen(exitTarget, this.options));
*///?} else if >=26.1 {
/*Screens.set(new OptionsScreen(exitTarget, this.options, ((Minecraft) (Object) this).level != null));
*///?} else
Screens.set(new OptionsScreen(exitTarget, this.options));
break;
}
}
@Override
public String getAccessToken() {
if (user == null) {
return null;
}
return this.user.getAccessToken();
}
@Override
public UUID getUuid() {
if (user == null) {
return null;
}
return this.user.getProfileId();
}
@Override
public String getUsername() {
if (user == null) {
return null;
}
return this.user.getName();
}
@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 = (Minecraft) (Object) this;
var handler = client.getConnection();
if (handler == null)
return null;
var entry = handler.getPlayerInfo(uuid);
if (entry == null)
return null;
//? if >=1.21.9 {
return entry.getProfile().name();
//?} else
//return entry.getProfile().getName();
}
@Override
@SuppressWarnings("resource")
public UUID getPlayerListEntryUUID(String name) {
var client = (Minecraft) (Object) this;
var handler = client.getConnection();
if (handler == null)
return null;
var entry = handler.getPlayerInfo(name);
if (entry == null)
return null;
//? if >=1.21.9 {
return entry.getProfile().id();
//?} else
//return entry.getProfile().getId();
}
}
@@ -0,0 +1,9 @@
package org.saturnclient.impl.mixins;
import org.joml.Quaternionf;
import org.saturnclient.common.ref.render.QuaternionfRef;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(Quaternionf.class)
public abstract class QuaternionfMixin implements QuaternionfRef {
}
@@ -0,0 +1,9 @@
package org.saturnclient.impl.mixins;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(TextureAtlasSprite.class)
public abstract class SpriteMixin implements SpriteRef {
}
@@ -0,0 +1,79 @@
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;
//? if <1.21.9
//import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
//? if >=26.2 {
/*import net.minecraft.client.gui.Hud;
*///?} else
import net.minecraft.client.gui.Gui;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.core.Holder;
import net.minecraft.resources.Identifier;
@Mixin(MobEffectInstance.class)
public abstract class StatusEffectMixin implements EffectRef {
@Shadow
public abstract boolean showIcon();
@Shadow
public abstract boolean isInfiniteDuration();
@Shadow
public abstract int getDuration();
@Shadow
public abstract Holder<MobEffect> getEffect();
// EffectRef's methods delegate to MobEffectInstance's, whose names production renames.
@Override
public boolean shouldShowIcon() {
return showIcon();
}
@Override
public boolean isInfinite() {
return isInfiniteDuration();
}
@Override
public SpriteRef getIcon() {
// 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.getGuiSprites().getSprite((Gui.getMobEffectSprite(getEffect())));
//?} else
//return (SpriteRef) SaturnClient.client.getMobEffectTextures().get(getEffect());
}
// Before 1.21.6 there's no InGameHud.getEffectTexture; getIcon's sprite is used there instead.
//? if >=1.21.6 {
@Override
public IdentifierRef getIconId() {
//? if >=26.2 {
/*Identifier id = Hud.getMobEffectSprite(getEffect());
*///?} else
Identifier id = Gui.getMobEffectSprite(getEffect());
return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png");
}
//?}
@Override
public String getInfiniteText() {
return "∞";
}
@Override
public int getDurationSeconds() {
return getDuration() / 20; // ticks → seconds
}
}
@@ -0,0 +1,29 @@
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.network.chat.Style;
import net.minecraft.network.chat.Component;
//? if >=1.21.9
import net.minecraft.network.chat.FontDescription.Resource;
import net.minecraft.resources.Identifier;
@Mixin(Component.class)
public interface TextMixin extends TextRef {
@Override
public default TextRef withFont(IdentifierRef font) {
//? if >=1.21.9 {
return (TextRef) (Component) (((Component) this).copy().setStyle(Style.EMPTY.withFont(new Resource((Identifier) (Object) font))));
//?} else
//return (TextRef) (((Component) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
}
@Override
public default int getWidth() {
return SaturnClient.client.font.width((Component) this);
}
}
@@ -0,0 +1,44 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.render.WindowRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import com.mojang.blaze3d.platform.Window;
@Mixin(Window.class)
public abstract class WindowMixin implements WindowRef {
@Shadow
public abstract int getScreenWidth();
@Shadow
public abstract int getScreenHeight();
// The framebuffer's size
@Shadow
public abstract int getWidth();
@Shadow
public abstract int getHeight();
// WindowRef's methods delegate to Window's, whose names production renames.
@Override
public int getWindowWidth() {
return getScreenWidth();
}
@Override
public int getWindowHeight() {
return getScreenHeight();
}
@Override
public int getFramebufferWidth() {
return getWidth();
}
@Override
public int getFramebufferHeight() {
return getHeight();
}
}
@@ -0,0 +1,68 @@
package org.saturnclient.impl.provider;
import net.minecraft.client.Minecraft;
import org.saturnclient.common.feature.*;
import org.saturnclient.common.provider.FeatureProvider;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.network.NetworkFeatureImpl;
import org.saturnclient.impl.features.player.PlayerFeatureImpl;
import org.saturnclient.impl.features.render.RenderFeatureImpl;
import org.saturnclient.impl.features.world.WorldFeatureImpl;
/**
* Fabric implementation of {@link FeatureProvider}.
*
* Instantiated once during client startup (e.g. in the mod initialiser)
* and passed to every feature constructor. All five sub-modules share
* the same {@link MinecraftClient} reference so there is no
* inconsistency between modules reading state within the same tick.
*
* <pre>{@code
* FeatureProvider provider = new FeatureProviderFabric(MinecraftClient.getInstance());
* FeatureManager.register(new AutoSprintFeature(provider));
* FeatureManager.register(new CoordinatesFeature(provider));
* // …
* }</pre>
*/
public class FeatureProviderFabric implements FeatureProvider {
private final PlayerFeature player;
private final WorldFeature world;
private final EntityFeature entity;
private final RenderFeature render;
private final NetworkFeature network;
public FeatureProviderFabric(Minecraft mc) {
this.player = new PlayerFeatureImpl(mc);
this.world = new WorldFeatureImpl(mc);
this.entity = new EntityFeatureImpl(mc);
this.render = new RenderFeatureImpl(mc);
this.network = new NetworkFeatureImpl(mc);
}
@Override
public PlayerFeature player() {
return player;
}
@Override
public WorldFeature world() {
return world;
}
@Override
public EntityFeature entity() {
return entity;
}
@Override
public RenderFeature render() {
return render;
}
@Override
public NetworkFeature network() {
return network;
}
}
@@ -0,0 +1,36 @@
package org.saturnclient.impl.provider;
// 26.3 uses SDL, not GLFW; Keys converts Saturn's GLFW key codes to its own.
//? if >=26.3 {
/*import com.mojang.blaze3d.platform.InputConstants;
*///?} else
import org.lwjgl.glfw.GLFW;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.saturnclient.SaturnClient;
public class GLFWProviderImpl implements GLFWProvider {
@Override
public boolean isKeyPressed(int key) {
//? if >=26.3 {
/*int gameKey = Keys.toGame(key);
return gameKey >= 0 && InputConstants.isKeyDown(gameKey) && Screens.current() == null;
*///?} else {
//? if >=1.21.9 {
long window = SaturnClient.client.getWindow().handle();
//?} else
//long window = SaturnClient.client.getWindow().getWindow();
return GLFW.glfwGetKey(window, key) == GLFW.GLFW_PRESS
&& Screens.current() == null;
//?}
}
@Override
public String getKeyName(int key) {
//? if >=26.3 {
/*int gameKey = Keys.toGame(key);
return gameKey < 0 ? null : InputConstants.Type.KEYBOARD.getOrCreate(gameKey).getDisplayName().getString();
*///?} else
return GLFW.glfwGetKeyName(key, GLFW.glfwGetKeyScancode(key));
}
}
@@ -0,0 +1,189 @@
package org.saturnclient.impl.provider;
//? if >=26.3 {
/*import com.mojang.blaze3d.platform.InputConstants;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import org.saturnclient.common.provider.GLFWProvider;
*///?}
/**
* Saturn's key codes are GLFW's, which the config's keybinds store. From 26.3 Minecraft uses SDL,
* whose keys (scancodes), modifiers and mouse buttons are numbered differently, so these convert
* between the two there. Before 26.3 Minecraft's numbers are GLFW's and pass through.
*/
public final class Keys {
private Keys() {
}
//? if >=26.3 {
/*private static final Int2IntOpenHashMap TO_GAME = new Int2IntOpenHashMap();
private static final Int2IntOpenHashMap TO_SATURN = new Int2IntOpenHashMap();
private static void key(int glfw, int game) {
TO_GAME.put(glfw, game);
TO_SATURN.put(game, glfw);
}
// GLFW's key codes, and the same keys' constants in 26.3
static {
TO_GAME.defaultReturnValue(-1);
TO_SATURN.defaultReturnValue(-1);
key(32, InputConstants.KEY_SPACE);
key(39, InputConstants.KEY_APOSTROPHE);
key(44, InputConstants.KEY_COMMA);
key(45, InputConstants.KEY_MINUS);
key(46, InputConstants.KEY_PERIOD);
key(47, InputConstants.KEY_SLASH);
key(48, InputConstants.KEY_0);
key(49, InputConstants.KEY_1);
key(50, InputConstants.KEY_2);
key(51, InputConstants.KEY_3);
key(52, InputConstants.KEY_4);
key(53, InputConstants.KEY_5);
key(54, InputConstants.KEY_6);
key(55, InputConstants.KEY_7);
key(56, InputConstants.KEY_8);
key(57, InputConstants.KEY_9);
key(59, InputConstants.KEY_SEMICOLON);
key(61, InputConstants.KEY_EQUALS);
key(65, InputConstants.KEY_A);
key(66, InputConstants.KEY_B);
key(67, InputConstants.KEY_C);
key(68, InputConstants.KEY_D);
key(69, InputConstants.KEY_E);
key(70, InputConstants.KEY_F);
key(71, InputConstants.KEY_G);
key(72, InputConstants.KEY_H);
key(73, InputConstants.KEY_I);
key(74, InputConstants.KEY_J);
key(75, InputConstants.KEY_K);
key(76, InputConstants.KEY_L);
key(77, InputConstants.KEY_M);
key(78, InputConstants.KEY_N);
key(79, InputConstants.KEY_O);
key(80, InputConstants.KEY_P);
key(81, InputConstants.KEY_Q);
key(82, InputConstants.KEY_R);
key(83, InputConstants.KEY_S);
key(84, InputConstants.KEY_T);
key(85, InputConstants.KEY_U);
key(86, InputConstants.KEY_V);
key(87, InputConstants.KEY_W);
key(88, InputConstants.KEY_X);
key(89, InputConstants.KEY_Y);
key(90, InputConstants.KEY_Z);
key(91, InputConstants.KEY_LBRACKET);
key(92, InputConstants.KEY_BACKSLASH);
key(93, InputConstants.KEY_RBRACKET);
key(96, InputConstants.KEY_GRAVE);
key(256, InputConstants.KEY_ESCAPE);
key(257, InputConstants.KEY_RETURN);
key(258, InputConstants.KEY_TAB);
key(259, InputConstants.KEY_BACKSPACE);
key(260, InputConstants.KEY_INSERT);
key(261, InputConstants.KEY_DELETE);
key(262, InputConstants.KEY_RIGHT);
key(263, InputConstants.KEY_LEFT);
key(264, InputConstants.KEY_DOWN);
key(265, InputConstants.KEY_UP);
key(266, InputConstants.KEY_PAGEUP);
key(267, InputConstants.KEY_PAGEDOWN);
key(268, InputConstants.KEY_HOME);
key(269, InputConstants.KEY_END);
key(280, InputConstants.KEY_CAPSLOCK);
key(281, InputConstants.KEY_SCROLLLOCK);
key(282, InputConstants.KEY_NUMLOCK);
key(283, InputConstants.KEY_PRINTSCREEN);
key(284, InputConstants.KEY_PAUSE);
key(290, InputConstants.KEY_F1);
key(291, InputConstants.KEY_F2);
key(292, InputConstants.KEY_F3);
key(293, InputConstants.KEY_F4);
key(294, InputConstants.KEY_F5);
key(295, InputConstants.KEY_F6);
key(296, InputConstants.KEY_F7);
key(297, InputConstants.KEY_F8);
key(298, InputConstants.KEY_F9);
key(299, InputConstants.KEY_F10);
key(300, InputConstants.KEY_F11);
key(301, InputConstants.KEY_F12);
key(302, InputConstants.KEY_F13);
key(303, InputConstants.KEY_F14);
key(304, InputConstants.KEY_F15);
key(305, InputConstants.KEY_F16);
key(306, InputConstants.KEY_F17);
key(307, InputConstants.KEY_F18);
key(308, InputConstants.KEY_F19);
key(309, InputConstants.KEY_F20);
key(310, InputConstants.KEY_F21);
key(311, InputConstants.KEY_F22);
key(312, InputConstants.KEY_F23);
key(313, InputConstants.KEY_F24);
key(320, InputConstants.KEY_NUMPAD0);
key(321, InputConstants.KEY_NUMPAD1);
key(322, InputConstants.KEY_NUMPAD2);
key(323, InputConstants.KEY_NUMPAD3);
key(324, InputConstants.KEY_NUMPAD4);
key(325, InputConstants.KEY_NUMPAD5);
key(326, InputConstants.KEY_NUMPAD6);
key(327, InputConstants.KEY_NUMPAD7);
key(328, InputConstants.KEY_NUMPAD8);
key(329, InputConstants.KEY_NUMPAD9);
key(330, InputConstants.KEY_NUMPADCOMMA);
key(332, InputConstants.KEY_MULTIPLY);
key(334, InputConstants.KEY_ADD);
key(335, InputConstants.KEY_NUMPADENTER);
key(336, InputConstants.KEY_NUMPADEQUALS);
key(340, InputConstants.KEY_LSHIFT);
key(341, InputConstants.KEY_LCONTROL);
key(342, InputConstants.KEY_LALT);
key(343, InputConstants.KEY_LGUI);
key(344, InputConstants.KEY_RSHIFT);
key(345, InputConstants.KEY_RCONTROL);
key(346, InputConstants.KEY_RALT);
key(347, InputConstants.KEY_RGUI);
}
*///?}
/** A Saturn (GLFW) key code as the game's, or -1 */
public static int toGame(int key) {
//? if >=26.3 {
/*return TO_GAME.get(key);
*///?} else
return key;
}
/** A key code from the game as Saturn's (GLFW), or -1 */
public static int toSaturn(int key) {
//? if >=26.3 {
/*return TO_SATURN.get(key);
*///?} else
return key;
}
/** The game's modifier flags as GLFW's */
public static int modifiersToSaturn(int modifiers) {
//? if >=26.3 {
/*int glfw = 0;
if ((modifiers & InputConstants.MOD_SHIFT) != 0) glfw |= GLFWProvider.GLFW_MOD_SHIFT;
if ((modifiers & InputConstants.MOD_CONTROL) != 0) glfw |= GLFWProvider.GLFW_MOD_CONTROL;
if ((modifiers & InputConstants.MOD_ALT) != 0) glfw |= GLFWProvider.GLFW_MOD_ALT;
if ((modifiers & InputConstants.MOD_SUPER) != 0) glfw |= GLFWProvider.GLFW_MOD_SUPER;
if ((modifiers & InputConstants.MOD_CAPS_LOCK) != 0) glfw |= GLFWProvider.GLFW_MOD_CAPS_LOCK;
if ((modifiers & InputConstants.MOD_NUM_LOCK) != 0) glfw |= GLFWProvider.GLFW_MOD_NUM_LOCK;
return glfw;
*///?} else
return modifiers;
}
/** A mouse button from the game as GLFW's (left 0, right 1, middle 2, then the side buttons) */
public static int mouseButtonToSaturn(int button) {
//? if >=26.3 {
/*if (button == InputConstants.MOUSE_BUTTON_LEFT) return 0;
if (button == InputConstants.MOUSE_BUTTON_RIGHT) return 1;
if (button == InputConstants.MOUSE_BUTTON_MIDDLE) return 2;
return button - 1;
*///?} else
return button;
}
}
@@ -0,0 +1,25 @@
package org.saturnclient.impl.provider;
import org.saturnclient.common.provider.RefConstructorProvider;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.render.TextRef;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
public class RefConstructorImpl implements RefConstructorProvider {
@Override
public IdentifierRef identifier(String fullPath) {
return (IdentifierRef) (Object) Identifier.parse(fullPath);
}
@Override
public IdentifierRef identifier(String namespace, String path) {
return (IdentifierRef) (Object) Identifier.fromNamespaceAndPath(namespace, path);
}
@Override
public TextRef text(String text) {
return (TextRef) (Component) Component.literal(text);
}
}
@@ -0,0 +1,102 @@
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.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
//? if >=1.21.9 {
import net.minecraft.world.entity.player.PlayerSkin;
//?} else
//import net.minecraft.client.resources.PlayerSkin;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.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.SystemToastId WARNING_TOAST = new SystemToast.SystemToastId(15_000L);
@Override
public IdentifierRef getPlayerSkin() {
if (playerSkin == null) {
Minecraft client = Minecraft.getInstance();
// 1.21.9 moved SkinTextures and renamed the skin provider's lookup.
//? if >=1.21.9 {
Supplier<PlayerSkin> textures = client.getSkinManager().createLookup(client.getGameProfile(), false);
playerSkin = () -> textures.get().body().texturePath();
//?} else {
/*Supplier<PlayerSkin> textures = client.getSkinManager().lookupInsecure(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) {
Minecraft client = Minecraft.getInstance();
//? if >=26.2 {
/*client.execute(() -> SystemToast.add(client.gui.toastManager(), WARNING_TOAST, Component.literal(title),
Component.literal(message)));
*///?} else {
client.execute(() -> client.getToastManager().addToast(
SystemToast.multiline(client, WARNING_TOAST, Component.literal(title), Component.literal(message))));
//?}
}
@Override
public void registerBufferedImageTexture(IdentifierRef i, BufferedImage bi) {
IdentifierUtils.registerBufferedImageTextureFast(i, bi);
}
}
@@ -0,0 +1,44 @@
package org.saturnclient.impl.ref;
import org.joml.Matrix3x2fStack;
import org.saturnclient.common.ref.render.MatrixStackRef;
public class Matrix3x2fStackRef implements MatrixStackRef {
public final Matrix3x2fStack stack;
public Matrix3x2fStackRef(Matrix3x2fStack stack) {
this.stack = stack;
}
@Override
public void push() {
stack.pushMatrix();
}
@Override
public void pop() {
stack.popMatrix();
}
@Override
public void scale(float x, float y) {
stack.scale(x, y);
}
@Override
public void translate(float x, float y) {
stack.translate(x, y);
}
@Override
public void rotate(float angle) {
stack.rotate((float) Math.toRadians(angle));
}
@Override
public void rotate(float angle, float originX, float originY, float originZ) {
translate(originX, originY);
rotate(angle);
translate(-originX, -originY);
}
}
@@ -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.renderer.entity.EntityRenderDispatcher;
//?} else
//import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.world.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.yBodyRot;
float k = entity.getYRot();
float l = entity.getXRot();
float m = entity.yHeadRotO;
float n = entity.yHeadRot;
entity.yBodyRot = 180.0F + yaw;
entity.setYRot(180.0F + yaw);
entity.setXRot(0.0F);
entity.yHeadRot = entity.getYRot();
entity.yHeadRotO = entity.getYRot();
float o = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getBbHeight() / 2.0F + scale * o, 0.0F);
float p = (float) size / o;
drawEntity(renderScope, x1, y1, x2, y2, p, vector3f, quaternionf, quaternionf2, entity);
entity.yBodyRot = j;
entity.setYRot(k);
entity.setXRot(l);
entity.yHeadRotO = m;
entity.yHeadRot = 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 {
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
//?} else
//EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
EntityRenderer<? super LivingEntity, ?> entityRenderer = entityRenderDispatcher.getRenderer(entity);
EntityRenderState entityRenderState = entityRenderer.createRenderState(entity, 1.0F);
//? if <1.21.11
//entityRenderState.hitboxesRenderState = 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 com.mojang.blaze3d.platform.Lighting;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.world.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.yBodyRot;
float l = entity.getYRot();
float m = entity.getXRot();
//? if >=1.21.5
float n = entity.yHeadRotO;
//? if <1.21.5
//float n = entity.yHeadRotO;
float o = entity.yHeadRot;
entity.setYRot(180.0F + yaw);
entity.yBodyRot = 180.0F + yaw;
entity.setXRot(0.0F);
entity.yHeadRot = entity.getYRot();
//? if >=1.21.5
entity.yHeadRotO = entity.getYHeadRot();
//? if <1.21.5
//entity.yHeadRotO = entity.getYHeadRot();
entity.setCustomNameVisible(false);
float p = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getBbHeight() / 2.0F + f * p, 0.0F);
float q = (float) size / p;
drawEntity(renderScope, g, h, q, vector3f, quaternionf, null, entity);
entity.setCustomNameVisible(true);
entity.yBodyRot = k;
entity.setYRot(l);
entity.setXRot(m);
//? if >=1.21.5
entity.yHeadRotO = n;
//? if <1.21.5
//entity.yHeadRotO = n;
entity.yHeadRot = 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.pushPose();
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.mulPose(quaternionf);
scopeImpl.draw();
//? if >=1.21.5
Lighting.setupForFlatItems();
//? if <1.21.5
//Lighting.setupForEntityInInventory();
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
entityRenderDispatcher.setRenderShadow(false);
scopeImpl.draw((vertexConsumers) -> {
entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F,
scopeImpl.matrices, vertexConsumers, 15728880);
});
scopeImpl.draw();
entityRenderDispatcher.setRenderShadow(true);
scopeImpl.getMatrixStack().pop();
Lighting.setupFor3DItems();
}
}
@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,459 @@
package org.saturnclient.impl.ui;
// Glass: a copy of the game behind the screen, blurred, drawn through each panel's rounded rectangle
// texture as a mask. There's a copy for each blur strength in use (menus and widgets have their own),
// blurred with vanilla's blur at that radius, so it holds the world or panorama without the screen's
// own animated background blur.
//
// The GUI rewrites split this into three implementations:
// - 1.21.6+: the GUI is drawn after the screen builds it, so the copy is taken from the main
// framebuffer while the screen builds its GUI, and glass is a GUI element with its own pipeline
// (shaders/core/glass.fsh). The blur is vanilla's effect rebuilt with the radius set.
// - 1.21.5: the screen draws straight into the framebuffer, blurring it first, so the game is saved
// before the blur (saveBackground). Glass is a render layer with its own pipeline
// (shaders/core/glass_pre_1_21_6.fsh), and the blur takes the radius as it runs.
// - 1.21.4: as 1.21.5, with GL framebuffers copied by blitting, and a shader program
// (shaders/core/glass.json) instead of a pipeline.
//? if >=1.21.6 {
import java.io.Reader;
import java.util.HashMap;
//? if >=26.1
//import java.util.LinkedHashSet;
import java.util.Map;
//? if >=26.1
//import java.util.Set;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.FilterMode;
import com.mojang.blaze3d.textures.GpuTextureView;
import com.mojang.serialization.JsonOps;
import org.saturnclient.config.Config;
import org.saturnclient.saturnclient.SaturnClient;
import com.mojang.blaze3d.pipeline.RenderTarget;
import net.minecraft.client.renderer.PostChainConfig;
import net.minecraft.client.renderer.PostChain;
import net.minecraft.client.renderer.RenderPipelines;
import com.mojang.blaze3d.pipeline.TextureTarget;
import net.minecraft.client.renderer.LevelTargetBundle;
//? if >=26.2 {
/*import com.mojang.blaze3d.GpuFormat;
import net.minecraft.client.renderer.BindGroupLayouts;
*///?}
//? if >=26.1 {
/*import net.minecraft.client.renderer.Projection;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
*///?} else
import net.minecraft.client.renderer.CachedOrthoProjectionMatrixBuffer;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.client.gui.render.TextureSetup;
import com.mojang.blaze3d.resource.CrossFrameResourcePool;
import net.minecraft.resources.Identifier;
public final class GlassRenderer {
public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)
.withLocation(Identifier.fromNamespaceAndPath("saturnclient", "pipeline/glass"))
.withFragmentShader(Identifier.fromNamespaceAndPath("saturnclient", "core/glass"))
//? if >=26.2 {
/*.withBindGroupLayout(BindGroupLayouts.SAMPLER1)
*///?} else
.withSampler("Sampler1")
// The transforms block ends with LineWidth before 1.21.11 (see glass.fsh).
//? if <1.21.11
//.withShaderDefine("HAS_LINE_WIDTH")
// 26.3 compiles through SPIR-V and moved TextureMat up the transforms block.
//? if >=26.3
//.withShaderDefine("EXPLICIT_LOCATIONS").withShaderDefine("TEXTURE_MAT_SECOND")
.build();
private static final Identifier BLUR = Identifier.withDefaultNamespace("post_effect/blur.json");
//? if >=26.1 {
/*private static final Projection PROJECTION = new Projection();
private static final ProjectionMatrixBuffer PROJECTION_BUFFER = new ProjectionMatrixBuffer("saturn glass");
static {
// As vanilla sets up its post chains' projection; the chain sizes it when it runs.
PROJECTION.setupOrtho(0.1f, 1000f, 1f, 1f, false);
}
*///?} else
private static final CachedOrthoProjectionMatrixBuffer PROJECTION = new CachedOrthoProjectionMatrixBuffer("saturn glass", 0.1f, 1000f, false);
// For the blurs' intermediate targets, freed once unused for a few frames
private static final CrossFrameResourcePool POOL = new CrossFrameResourcePool(3);
// By blur strength. A blur that failed to load is kept as null, so it isn't retried every frame.
private static final Map<Integer, PostChain> BLURS = new HashMap<>();
private static final Map<Integer, TextureTarget> BACKDROPS = new HashMap<>();
private GlassRenderer() {
}
// From 26.1 the GUI is built before the frame's world is drawn, so the main framebuffer still
// holds the last frame, glass included. The copies wait until the GUI renderer is about to draw
// (GuiRendererMixin), when it holds this frame's world and panorama.
//? if >=26.1 {
/*private static final Set<Integer> PENDING = new LinkedHashSet<>();
*///?}
private static RenderTarget mainTarget() {
//? if >=26.2 {
/*return SaturnClient.client.gameRenderer.mainRenderTarget();
*///?} else
return SaturnClient.client.getMainRenderTarget();
}
// Copies the main framebuffer and blurs the copy by blur. Call once a frame for each blur, while
// the screen builds its GUI, before drawing glass with it.
public static void captureBackdrop(int blur) {
RenderTarget main = mainTarget();
int width = main.width;
int height = main.height;
TextureTarget backdrop = BACKDROPS.get(blur);
if (backdrop == null) {
// From 26.3 the depth format picks the depth buffer (none here); 26.2 takes a flag.
//? if >=26.3 {
/*backdrop = new TextureTarget("Saturn glass " + blur, width, height, GpuFormat.RGBA8_UNORM, null);
*///?} else if >=26.2 {
/*backdrop = new TextureTarget("Saturn glass " + blur, width, height, false, GpuFormat.RGBA8_UNORM);
*///?} else
backdrop = new TextureTarget("Saturn glass " + blur, width, height, false);
BACKDROPS.put(blur, backdrop);
} else if (backdrop.width != width || backdrop.height != height) {
backdrop.resize(width, height);
}
//? if <1.21.11
//backdrop.getColorTexture().setTextureFilter(FilterMode.LINEAR, false);
//? if >=26.1 {
/*PENDING.add(blur);
}
// Copies and blurs the backdrops the GUI asked for this frame. GuiRendererMixin calls it once the
// world and the panorama are drawn, before the GUI.
public static void blurPendingBackdrops() {
RenderTarget main = mainTarget();
for (int blur : PENDING) {
TextureTarget backdrop = BACKDROPS.get(blur);
// The GUI already refers to the backdrop's texture, so after a resize it keeps the last
// frame's until the next one.
if (backdrop != null && backdrop.width == main.width && backdrop.height == main.height) {
blurBackdrop(main, backdrop, blur);
}
}
PENDING.clear();
}
private static void blurBackdrop(RenderTarget main, TextureTarget backdrop, int blur) {
int width = main.width;
int height = main.height;
*///?}
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
main.getColorTexture(), backdrop.getColorTexture(), 0, 0, 0, 0, 0, width, height);
PostChain processor = getBlur(blur);
if (processor != null) {
processor.process(backdrop, POOL);
}
POOL.endFrame();
release(blur);
}
// The mask, then the copy blurred by blur, as the glass pipeline samples them
public static TextureSetup textures(AbstractTexture mask, int blur) {
GpuTextureView backdrop = BACKDROPS.get(blur).getColorTextureView();
//? if >=1.21.11
return TextureSetup.doubleTexture(mask.getTextureView(), mask.getSampler(), backdrop, RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR));
//? if <1.21.11
//return TextureSetup.doubleTexture(mask.getTextureView(), backdrop);
}
// Frees the copies and blurs of strengths that are no longer set, besides kept
private static void release(int kept) {
BACKDROPS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && !isSet(entry.getKey());
// The GUI already refers to this frame's pending backdrops.
//? if >=26.1
//unused &= !PENDING.contains(entry.getKey());
if (unused) {
entry.getValue().destroyBuffers();
}
return unused;
});
BLURS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && !isSet(entry.getKey());
// The GUI already refers to this frame's pending backdrops.
//? if >=26.1
//unused &= !PENDING.contains(entry.getKey());
if (unused && entry.getValue() != null) {
entry.getValue().close();
}
return unused;
});
}
private static boolean isSet(int blur) {
return blur == Config.menuBlur.value || blur == Config.widgetBlur.value;
}
private static PostChain getBlur(int blur) {
if (!BLURS.containsKey(blur)) {
BLURS.put(blur, loadBlur(blur));
}
return BLURS.get(blur);
}
// Vanilla's blur, with its radius set to blur. Vanilla's own leaves it at 0, which makes the shader
// read the menu blur setting (Saturn's animated background blur while a Saturn screen is open).
private static PostChain loadBlur(int blur) {
try (Reader reader = SaturnClient.client.getResourceManager().getResourceOrThrow(BLUR).openAsReader()) {
JsonElement json = JsonParser.parseReader(reader);
setRadius(json, blur);
PostChainConfig pipeline = PostChainConfig.CODEC.parse(JsonOps.INSTANCE, json).getOrThrow();
return PostChain.load(pipeline, SaturnClient.client.getTextureManager(),
//? if >=26.1 {
/*LevelTargetBundle.MAIN_TARGETS, Identifier.fromNamespaceAndPath("saturnclient", "glass_blur_" + blur), PROJECTION, PROJECTION_BUFFER);
*///?} else
LevelTargetBundle.MAIN_TARGETS, Identifier.fromNamespaceAndPath("saturnclient", "glass_blur_" + blur), PROJECTION);
} catch (Exception e) {
SaturnClient.LOGGER.error("Couldn't load the glass blur", e);
return null;
}
}
// Sets every "Radius" uniform in the effect
private static void setRadius(JsonElement json, int radius) {
if (json.isJsonArray()) {
json.getAsJsonArray().forEach(element -> setRadius(element, radius));
} else if (json.isJsonObject()) {
JsonObject object = json.getAsJsonObject();
if (object.has("name") && object.get("name").getAsString().equals("Radius")) {
object.addProperty("value", (float) radius);
}
object.entrySet().forEach(entry -> setRadius(entry.getValue(), radius));
}
}
}
//?} else if >=1.21.5 {
/*import java.util.HashMap;
import java.util.Map;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.FilterMode;
import org.saturnclient.config.Config;
import org.saturnclient.saturnclient.SaturnClient;
import com.mojang.blaze3d.pipeline.RenderTarget;
import net.minecraft.client.renderer.PostChain;
import net.minecraft.client.renderer.RenderPipelines;
import com.mojang.blaze3d.pipeline.TextureTarget;
import net.minecraft.client.renderer.LevelTargetBundle;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderStateShard;
import com.mojang.blaze3d.resource.CrossFrameResourcePool;
import net.minecraft.resources.Identifier;
public final class GlassRenderer {
private static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)
.withLocation(Identifier.fromNamespaceAndPath("saturnclient", "pipeline/glass"))
.withFragmentShader(Identifier.fromNamespaceAndPath("saturnclient", "core/glass_pre_1_21_6"))
.withSampler("Sampler1")
.build();
private static final Identifier BLUR = Identifier.withDefaultNamespace("blur");
// For the blur's intermediate target, freed once unused for a few frames
private static final CrossFrameResourcePool POOL = new CrossFrameResourcePool(3);
private static final Map<Integer, TextureTarget> BACKDROPS = new HashMap<>();
// By mask and blur
private static final Map<String, RenderType> LAYERS = new HashMap<>();
// The game behind the screen before the screen blurs it, which glass is blurred from
private static TextureTarget background;
private GlassRenderer() {
}
// Saves the main framebuffer for glass. Call each frame before the screen blurs its background.
public static void saveBackground() {
background = copy(SaturnClient.client.getMainRenderTarget(), background, "Saturn glass background");
}
// Copies the saved background and blurs the copy by blur. Call once a frame for each blur, before
// drawing glass with it.
public static void captureBackdrop(int blur) {
RenderTarget from = background != null ? background : SaturnClient.client.getMainRenderTarget();
TextureTarget backdrop = copy(from, BACKDROPS.get(blur), "Saturn glass " + blur);
BACKDROPS.put(blur, backdrop);
PostChain processor = SaturnClient.client.getShaderManager().getPostChain(BLUR,
LevelTargetBundle.MAIN_TARGETS);
if (processor != null) {
processor.process(backdrop, POOL, pass -> pass.setUniform("Radius", (float) blur));
}
POOL.endFrame();
release(blur);
}
// Draws glass through mask with the copy blurred by blur
public static RenderType getLayer(Identifier mask, int blur) {
return LAYERS.computeIfAbsent(mask + " " + blur, key -> RenderType.create("saturn_glass", 786432, PIPELINE,
RenderType.CompositeState.builder()
.setTextureState(new RenderStateShard.EmptyTextureStateShard(() -> {
RenderSystem.setShaderTexture(0,
SaturnClient.client.getTextureManager().getTexture(mask).getTexture());
RenderSystem.setShaderTexture(1, BACKDROPS.get(blur).getColorTexture());
}, () -> {
}))
.createCompositeState(false)));
}
// Frees the copies of strengths that are no longer set, besides kept
private static void release(int kept) {
BACKDROPS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && entry.getKey() != Config.menuBlur.value
&& entry.getKey() != Config.widgetBlur.value;
if (unused) {
entry.getValue().destroyBuffers();
}
return unused;
});
}
// Copies from into to, made or resized to match
private static TextureTarget copy(RenderTarget from, TextureTarget to, String name) {
int width = from.width;
int height = from.height;
if (to == null) {
to = new TextureTarget(name, width, height, false);
} else if (to.width != width || to.height != height) {
to.resize(width, height);
}
to.getColorTexture().setTextureFilter(FilterMode.LINEAR, false);
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
from.getColorTexture(), to.getColorTexture(), 0, 0, 0, 0, 0, width, height);
return to;
}
}
*///?} else {
/*import java.util.HashMap;
import java.util.Map;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import org.saturnclient.config.Config;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.renderer.ShaderDefines;
import com.mojang.blaze3d.pipeline.RenderTarget;
import net.minecraft.client.renderer.PostChain;
import net.minecraft.client.renderer.ShaderProgram;
import com.mojang.blaze3d.pipeline.TextureTarget;
import net.minecraft.client.renderer.LevelTargetBundle;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderStateShard;
import com.mojang.blaze3d.vertex.VertexFormat;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.resource.CrossFrameResourcePool;
import net.minecraft.resources.Identifier;
public final class GlassRenderer {
private static final ShaderProgram PROGRAM = new ShaderProgram(Identifier.fromNamespaceAndPath("saturnclient", "core/glass"),
DefaultVertexFormat.POSITION_TEX_COLOR, ShaderDefines.EMPTY);
private static final Identifier BLUR = Identifier.withDefaultNamespace("blur");
// For the blur's intermediate target, freed once unused for a few frames
private static final CrossFrameResourcePool POOL = new CrossFrameResourcePool(3);
private static final Map<Integer, TextureTarget> BACKDROPS = new HashMap<>();
// By mask and blur
private static final Map<String, RenderType> LAYERS = new HashMap<>();
// The game behind the screen before the screen blurs it, which glass is blurred from
private static TextureTarget background;
private GlassRenderer() {
}
// Saves the main framebuffer for glass. Call each frame before the screen blurs its background.
public static void saveBackground() {
background = copy(SaturnClient.client.getMainRenderTarget(), background);
}
// Copies the saved background and blurs the copy by blur. Call once a frame for each blur, before
// drawing glass with it.
public static void captureBackdrop(int blur) {
RenderTarget from = background != null ? background : SaturnClient.client.getMainRenderTarget();
TextureTarget backdrop = copy(from, BACKDROPS.get(blur));
BACKDROPS.put(blur, backdrop);
PostChain processor = SaturnClient.client.getShaderManager().getPostChain(BLUR,
LevelTargetBundle.MAIN_TARGETS);
if (processor != null) {
processor.setUniform("Radius", blur);
processor.process(backdrop, POOL);
}
POOL.endFrame();
release(blur);
SaturnClient.client.getMainRenderTarget().bindWrite(false);
}
// Draws glass through mask with the copy blurred by blur
public static RenderType getLayer(Identifier mask, int blur) {
return LAYERS.computeIfAbsent(mask + " " + blur, key -> RenderType.create("saturn_glass",
DefaultVertexFormat.POSITION_TEX_COLOR, VertexFormat.Mode.QUADS, 786432,
RenderType.CompositeState.builder()
.setTextureState(new RenderStateShard.EmptyTextureStateShard(() -> {
RenderSystem.setShaderTexture(0, mask);
RenderSystem.setShaderTexture(1, BACKDROPS.get(blur).getColorTextureId());
}, () -> {
}))
.setShaderState(new RenderStateShard.ShaderStateShard(PROGRAM))
.setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY)
.createCompositeState(false)));
}
// Frees the copies of strengths that are no longer set, besides kept
private static void release(int kept) {
BACKDROPS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && entry.getKey() != Config.menuBlur.value
&& entry.getKey() != Config.widgetBlur.value;
if (unused) {
entry.getValue().destroyBuffers();
}
return unused;
});
}
// Copies from into to, made or resized to match
private static TextureTarget copy(RenderTarget from, TextureTarget to) {
int width = from.width;
int height = from.height;
if (to == null) {
to = new TextureTarget(width, height, false);
} else if (to.width != width || to.height != height) {
to.resize(width, height);
}
to.setFilterMode(GL11.GL_LINEAR);
GlStateManager._glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, from.frameBufferId);
GlStateManager._glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, to.frameBufferId);
GlStateManager._glBlitFrameBuffer(0, 0, width, height, 0, 0, width, height, GL11.GL_COLOR_BUFFER_BIT,
GL11.GL_NEAREST);
SaturnClient.client.getMainRenderTarget().bindWrite(false);
return to;
}
}
*///?}
@@ -0,0 +1,842 @@
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.HashSet;
import java.util.Objects;
import java.util.Set;
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.renderer.RenderPipelines;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.state.ColoredRectangleRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.GuiTextRenderState;
import net.minecraft.client.gui.render.state.BlitRenderState;
import net.minecraft.client.gui.render.state.pip.GuiEntityRenderState;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
//? if >=1.21.7 {
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
//?} else
//import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.gui.render.TextureSetup;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.ReportedException;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.world.level.Level;
public class RenderScopeImpl implements RenderScope {
public final Matrix3x2fStackRef matrices;
public final GuiRenderState state;
private ScissorStack scissorStack;
private int opacity = 0xFF << 24;
// The glass blurs made for this scope's frame
private final Set<Integer> capturedBlurs = new HashSet<>();
public RenderScopeImpl(GuiGraphics context) {
this(context.pose(), ((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.submitGuiElement(
new ColoredRectangleRenderState(
RenderPipelines.GUI,
TextureSetup.noTexture(),
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.submitText(
new GuiTextRenderState(
SaturnClient.client.font,
((Component) Fonts.setFont(line, font)).getVisualOrderText(),
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().getGuiScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getGuiScaledHeight();
}
// 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).getTextureView();
//? if >=1.21.11
AbstractTexture abstractTexture = SaturnClient.client.getTextureManager().getTexture(sprite);
this.state.submitGuiElement(new BlitRenderState(
RenderPipelines.GUI_TEXTURED,
//? if >=1.21.11 {
TextureSetup.singleTexture(gpuTextureView, abstractTexture.getSampler()),
//?} else
//TextureSetup.singleTexture(gpuTextureView),
new Matrix3x2f(this.matrices.stack),
x1, y1, x2, y2, u1, u2, v1, v2, color,
this.scissorStack.peekLast()));
}
@Override
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
int color = getColor(0xFFFFFFFF);
if (mask == null || color >>> 24 == 0)
return;
if (capturedBlurs.add(blur)) {
GlassRenderer.captureBackdrop(blur);
}
AbstractTexture maskTexture = SaturnClient.client.getTextureManager().getTexture((Identifier) (Object) mask);
this.state.submitGuiElement(new BlitRenderState(
GlassRenderer.PIPELINE,
GlassRenderer.textures(maskTexture, blur),
new Matrix3x2f(this.matrices.stack),
x, y, x + width, y + height, 0, 1, 0, 1, color,
this.scissorStack.peekLast()));
}
// Scissor management
@Override
public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRectangle rect = new ScreenRectangle(x1, y1, x2 - x1, y2 - y1).transformAxisAligned(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<ScreenRectangle> stack = new ArrayDeque<>();
public ScreenRectangle push(ScreenRectangle rect) {
ScreenRectangle last = stack.peekLast();
if (last != null) {
ScreenRectangle intersection = Objects.requireNonNullElse(rect.intersection(last), ScreenRectangle.empty());
stack.addLast(intersection);
return intersection;
} else {
stack.addLast(rect);
return rect;
}
}
@Nullable
public ScreenRectangle pop() {
if (stack.isEmpty())
throw new IllegalStateException("Scissor stack underflow");
stack.removeLast();
return stack.peekLast();
}
@Nullable
public ScreenRectangle peekLast() {
return stack.peekLast();
}
public boolean contains(int x, int y) {
return stack.isEmpty() || stack.peekLast().containsPoint(x, y);
}
}
// Item rendering
@Override
public void drawItem(ItemStackRef stack, int x, int y) {
drawItem(null, SaturnClient.client.level, stack, x, y, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable Level 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 Level world, ItemStack stack, int x, int y, int seed,
int z) {
if (!stack.isEmpty()) {
//? if >=1.21.7 {
TrackingItemStackRenderState keyedItemRenderState = new TrackingItemStackRenderState();
//?} else
//ItemStackRenderState keyedItemRenderState = new ItemStackRenderState();
SaturnClient.client.getItemModelResolver().updateForTopItem(keyedItemRenderState, stack,
ItemDisplayContext.GUI, world,
entity, seed);
try {
//? if >=26.1 {
/*this.state.submitItem(new GuiItemRenderState(
*///?} else
this.state.submitItem(new GuiItemRenderState(stack.getItem().getName().toString(),
new Matrix3x2f(this.matrices.stack), keyedItemRenderState, x, y,
this.scissorStack.peekLast()));
} catch (Throwable throwable) {
CrashReport crashReport = CrashReport.forThrowable(throwable, "Rendering item");
CrashReportCategory crashReportSection = crashReport.addCategory("Item being rendered");
crashReportSection.setDetail("Item Type", () -> String.valueOf(stack.getItem()));
crashReportSection.setDetail("Item Components", () -> String.valueOf(stack.getComponents()));
crashReportSection.setDetail("Item Foil", () -> String.valueOf(stack.hasFoil()));
throw new ReportedException(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) {
TextureAtlasSprite mcSprite = (TextureAtlasSprite) sprite;
if (color == 0 || width == 0 || height == 0)
return;
drawTexturedQuad(mcSprite.atlasLocation(), x, x + width, y, y + height,
mcSprite.getU0(), mcSprite.getU1(), mcSprite.getV0(), mcSprite.getV1(), 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.submitPicturesInPictureState(new GuiEntityRenderState(entityState, translation, rotation,
overrideCameraAngle, adjustedX1, adjustedY1, adjustedX2, adjustedY2, adjustedScale,
this.scissorStack.peekLast()));
}
}
//?} else {
/*import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
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.gui.Font;
import net.minecraft.client.gui.Font.DisplayMode;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import com.mojang.blaze3d.platform.Lighting;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.client.renderer.RenderType;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import com.mojang.blaze3d.platform.Window;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.world.entity.LivingEntity;
//? if >=1.21.5
import net.minecraft.world.item.ItemDisplayContext;
//? if <1.21.5
//import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.ReportedException;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.world.level.Level;
public class RenderScopeImpl implements RenderScope {
// ----------------------------
// Fields
// ----------------------------
public PoseStack matrices;
public MultiBufferSource.BufferSource vertexConsumers;
private ScissorStack scissorStack = new ScissorStack();
private int opacity = 255 << 24;
// The glass blurs made for this scope's frame
private final Set<Integer> capturedBlurs = new HashSet<>();
private final ItemStackRenderState itemRenderState;
// ----------------------------
// Constructors
// ----------------------------
public RenderScopeImpl(GuiGraphics context) {
this(context.pose(), ((DrawContextAccessor) context).getVertexConsumers());
}
public RenderScopeImpl(PoseStack matrices, MultiBufferSource.BufferSource vertexConsumers) {
this.matrices = matrices;
this.vertexConsumers = vertexConsumers;
this.scissorStack = new ScissorStack();
this.itemRenderState = new ItemStackRenderState();
}
// ----------------------------
// 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.endBatch();
}
@Override
public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getGuiScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getGuiScaledHeight();
}
public void draw(Consumer<MultiBufferSource> drawer) {
drawer.accept(this.vertexConsumers);
this.vertexConsumers.endBatch();
}
// ----------------------------
// 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.last().pose();
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(RenderType.gui());
vertexConsumer.addVertex(matrix4f, x1, y1, 0).setColor(color);
vertexConsumer.addVertex(matrix4f, x1, y2, 0).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y2, 0).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y1, 0).setColor(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(RenderType.gui(), x1, y1, x2, y2, z, color);
}
public void fill(RenderType layer, int x1, int y1, int x2, int y2, int color) {
fill(layer, x1, y1, x2, y2, 0, color);
}
public void fill(RenderType layer, int x1, int y1, int x2, int y2, int z, int color) {
Matrix4f matrix4f = this.matrices.last().pose();
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.addVertex(matrix4f, x1, y1, z).setColor(color);
vertexConsumer.addVertex(matrix4f, x1, y2, z).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y2, z).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y1, z).setColor(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.pushPose();
matrices.translate(x, y + i * Fonts.getHeight(), 0);
matrices.scale(scale, scale, 1.0f);
Font textRenderer = SaturnClient.client.font;
textRenderer.drawInBatch((Component) Fonts.setFont(line, font), 0, font == 0 ? 1 : 7, color, false,
matrices.last().pose(), vertexConsumers, DisplayMode.NORMAL, 0, 15728880);
matrices.popPose();
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) {
TextureAtlasSprite sprite = (TextureAtlasSprite) saturnSprite;
if (color == 0)
return;
if (width != 0 && height != 0) {
drawTexturedQuad(sprite.atlasLocation(), x, x + width, y, y + height,
sprite.getU0(), sprite.getU1(), sprite.getV0(), sprite.getV1(), 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.pushPose();
matrices.scale(0.25f, 0.25f, 1.0f);
//? if <1.21.5
//RenderSystem.setShaderTexture(0, sprite);
RenderType renderLayer = RenderType.guiTextured(sprite);
Matrix4f matrix4f = matrices.last().pose();
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);
vertexConsumer.addVertex(matrix4f, x1, y1, 0).setUv(u1, v1).setColor(color);
vertexConsumer.addVertex(matrix4f, x1, y2, 0).setUv(u1, v2).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y2, 0).setUv(u2, v2).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y1, 0).setUv(u2, v1).setColor(color);
matrices.popPose();
}
@Override
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
int color = getColor(0xFFFFFFFF);
if (mask == null || color >>> 24 == 0)
return;
if (capturedBlurs.add(blur)) {
GlassRenderer.captureBackdrop(blur);
}
// In quarter units, as drawTexturedQuad draws
int x1 = x * 4;
int x2 = (x + width) * 4;
int y1 = y * 4;
int y2 = (y + height) * 4;
matrices.pushPose();
matrices.scale(0.25f, 0.25f, 1.0f);
Matrix4f matrix4f = matrices.last().pose();
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(GlassRenderer.getLayer((Identifier) (Object) mask, blur));
vertexConsumer.addVertex(matrix4f, x1, y1, 0).setUv(0, 0).setColor(color);
vertexConsumer.addVertex(matrix4f, x1, y2, 0).setUv(0, 1).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y2, 0).setUv(1, 1).setColor(color);
vertexConsumer.addVertex(matrix4f, x2, y1, 0).setUv(1, 0).setColor(color);
matrices.popPose();
}
// ----------------------------
// Items
// ----------------------------
@Override
public void drawItem(ItemStackRef item, int x, int y) {
drawItem(SaturnClient.client.player, SaturnClient.client.level, item, x, y, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable Level 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 Level world, ItemStack stack, int x, int y, int seed,
int z) {
if (stack.isEmpty())
return;
//? if >=1.21.5
SaturnClient.client.getItemModelResolver().appendItemLayers(itemRenderState, stack, ItemDisplayContext.GUI, world, entity, seed);
//? if >=1.21.5
SaturnClient.client.getItemModelResolver().updateForTopItem(this.itemRenderState, stack, ItemDisplayContext.GUI, world, entity, seed);
//? if <1.21.5
//SaturnClient.client.getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, false, world, entity, seed);
this.matrices.pushPose();
//? 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.isGui3d() ? z : 0));
try {
this.matrices.scale(16.0F, -16.0F, 16.0F);
boolean bl = !this.itemRenderState.usesBlockLight();
if (bl) {
this.draw();
Lighting.setupForFlatItems();
}
this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.NO_OVERLAY);
this.draw();
if (bl) {
Lighting.setupFor3DItems();
}
} catch (Throwable throwable) {
CrashReport crashReport = CrashReport.forThrowable(throwable, "Rendering item");
CrashReportCategory crashReportSection = crashReport.addCategory("Item being rendered");
crashReportSection.setDetail("Item Type", () -> String.valueOf(stack.getItem()));
crashReportSection.setDetail("Item Components", () -> String.valueOf(stack.getComponents()));
crashReportSection.setDetail("Item Foil", () -> String.valueOf(stack.hasFoil()));
throw new ReportedException(crashReport);
}
this.matrices.popPose();
}
// ----------------------------
// Scissor / Clipping
// ----------------------------
@Override
public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRectangle screenRect = new ScreenRectangle(x1, y1, x2 - x1, y2 - y1)
.transformAxisAligned(matrices.last().pose());
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 ScreenRectangle rect) {
draw();
if (rect != null) {
Window window = SaturnClient.client.getWindow();
int framebufferHeight = window.getHeight();
double scaleFactor = window.getGuiScale();
RenderSystem.enableScissor(
(int) (rect.left() * scaleFactor),
(int) (framebufferHeight - rect.bottom() * scaleFactor),
Math.max(0, (int) (rect.width() * scaleFactor)),
Math.max(0, (int) (rect.height() * scaleFactor)));
} else {
RenderSystem.disableScissor();
}
}
static class ScissorStack {
private final Deque<ScreenRectangle> stack = new ArrayDeque<>();
public ScreenRectangle push(ScreenRectangle p_281812_) {
ScreenRectangle screenrectangle = this.stack.peekLast();
if (screenrectangle != null) {
ScreenRectangle screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
ScreenRectangle.empty());
this.stack.addLast(screenrectangle1);
return screenrectangle1;
} else {
this.stack.addLast(p_281812_);
return p_281812_;
}
}
@Nullable
public ScreenRectangle 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().containsPoint(p_329411_, p_333404_);
}
}
}
*///?}
@@ -0,0 +1,297 @@
package org.saturnclient.impl.ui;
import java.time.Duration;
import java.time.Instant;
import org.saturnclient.config.Config;
import org.saturnclient.impl.provider.Keys;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.SaturnScreen.ScreenProvider;
import net.minecraft.client.Minecraft;
//? if >=1.21.9
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.renderer.CubeMap;
import net.minecraft.client.gui.GuiGraphics;
//? if >=26.1 {
/*import net.minecraft.client.renderer.Panorama;
*///?} else
import net.minecraft.client.renderer.PanoramaRenderer;
import net.minecraft.client.gui.screens.Screen;
//? if >=1.21.6 {
import net.minecraft.client.renderer.texture.CubeMapTexture;
//?} else {
/*import net.minecraft.client.renderer.texture.SimpleTexture;
import net.minecraft.client.renderer.texture.TextureManager;
*///?}
//? if >=1.21.9
import net.minecraft.client.input.KeyEvent;
//? if <1.21.6 {
/*import net.minecraft.client.renderer.PostChain;
import net.minecraft.client.renderer.LevelTargetBundle;
import com.mojang.blaze3d.resource.CrossFrameResourcePool;
*///?}
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
public class SaturnScreenFabric extends Screen implements ScreenProvider {
protected static final Identifier PANORAMA = Identifier.fromNamespaceAndPath("saturnclient",
"textures/gui/title/background/panorama");
public static final CubeMap PANORAMA_RENDERER = new CubeMap(PANORAMA);
// From 26.1 vanilla draws the panorama from its own cube map; GuiRendererMixin swaps in Saturn's.
//? if >=26.1 {
/*public static final Panorama ROTATING_PANORAMA_RENDERER = new Panorama();
*///?} else {
public static final PanoramaRenderer ROTATING_PANORAMA_RENDERER;
static {
ROTATING_PANORAMA_RENDERER = new PanoramaRenderer(PANORAMA_RENDERER);
}
//?}
//? if <1.21.6
//private final CrossFrameResourcePool pool = new CrossFrameResourcePool(3);
public final SaturnScreen screen;
/** Whether the panorama's textures are loaded; it isn't drawn before (e.g. under the splash). */
private static volatile boolean panoramaReady = false;
public static void preload(Minecraft 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().registerAndLoad(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");
SimpleTexture resourceTexture = new SimpleTexture(tex);
textureManager.registerAndLoad(tex, resourceTexture);
}
*///?}
panoramaReady = true;
}
public SaturnScreenFabric(SaturnScreen screen) {
super(Component.literal(screen.title));
screen.provider = this;
this.screen = screen;
}
@Override
protected void init() {
screen.init();
}
//? if <1.21.6
//@SuppressWarnings("deprecation")
@Override
//? if >=26.1 {
/*public void extractRenderState(GuiGraphics context, int mouseX, int mouseY, float delta) {
*///?} else
public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {
if (screen.start == null) {
screen.start = Instant.now();
}
long elapsed = Duration.between(screen.start, Instant.now()).toMillis();
screen.updateScale();
mouseX = (int) (mouseX / screen.scale);
mouseY = (int) (mouseY / screen.scale);
// 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);
}
// Glass is blurred from the game as it is before this screen blurs it.
if (Config.usesGlass()) {
context.flush();
GlassRenderer.saveBackground();
}
float radius = screen.getBackgroundBlur();
PostChain postEffectProcessor = radius < 1 ? null : this.minecraft.getShaderManager().getPostChain(
Identifier.withDefaultNamespace("blur"),
LevelTargetBundle.MAIN_TARGETS);
if (postEffectProcessor != null) {
//? if >=1.21.5
postEffectProcessor.process(this.minecraft.getMainRenderTarget(), this.pool, pass -> pass.setUniform("Radius", radius));
//? if <1.21.5
//postEffectProcessor.setUniform("Radius", radius);
//? if <1.21.5
//postEffectProcessor.process(this.minecraft.getMainRenderTarget(), this.pool);
}
*///?}
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
//? if >=26.1 {
/*public void extractBackground(GuiGraphics context, int mouseX, int mouseY, float delta) {
*///?} else
public void renderBackground(GuiGraphics 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()) {
//? if >=26.2 {
/*ROTATING_PANORAMA_RENDERER.extractRenderState(context, this.width, this.height);
*///?} else if >=26.1 {
/*ROTATING_PANORAMA_RENDERER.extractRenderState(context, this.width, this.height, true);
*///?} else
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, true);
}
if (screen.getBackgroundBlur() >= 1) {
context.blurBeforeThisStratum();
}
if (minecraft.level != null) {
//? if >=26.1 {
/*extractMenuBackground(context);
*///?} else
renderMenuBackground(context);
}
}
//?}
/** Whether Saturn's panorama is behind the screen: outside a world, once it's loaded. */
private boolean isShowingPanorama() {
return panoramaReady && minecraft.level == null && minecraft.getCurrentServer() == null;
}
// 1.21.9 moved mouse and key events to Click and KeyInput.
//? if >=1.21.9 {
@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
if (screen.mouseClicked(click.x(), click.y(), Keys.mouseButtonToSaturn(click.button()))) {
return true;
}
return super.mouseClicked(click, doubled);
}
@Override
public boolean mouseDragged(MouseButtonEvent 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(), Keys.mouseButtonToSaturn(click.button()), offsetX, offsetY)) {
return true;
}
return super.mouseDragged(click, offsetX, offsetY);
}
@Override
public boolean mouseReleased(MouseButtonEvent click) {
if (screen.mouseReleased(click.x(), click.y(), Keys.mouseButtonToSaturn(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);
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
if (screen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
return true;
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
}
@Override
public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (screen.mouseReleased(mouseX, mouseY, button)) {
return true;
}
return super.mouseReleased(mouseX, mouseY, button);
}
*///?}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
if (screen.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
return true;
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
}
//? if >=1.21.9 {
@Override
public boolean keyPressed(KeyEvent input) {
// 26.3's key() is SDL's scancode; Saturn's key codes are GLFW's (see Keys).
//? if >=26.3 {
/*if (screen.keyPressed(Keys.toSaturn(input.key()), input.key(), Keys.modifiersToSaturn(input.modifiers()))) {
*///?} else
if (screen.keyPressed(input.input(), 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(Minecraft client, int width, int height) {
screen.resize(width, height);
super.resize(client, width, height);
}
*///?}
@Override
public void onClose() {
this.screen.onClose();
super.onClose();
}
// ScreenProvider's close, separate from the vanilla onClose above.
@Override
public void close() {
onClose();
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
}
@@ -0,0 +1,33 @@
package org.saturnclient.impl.ui;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
/** The open screen and overlay, which Minecraft holds before 26.2 and its Gui from 26.2. */
public final class Screens {
private Screens() {
}
public static @Nullable Screen current() {
//? if >=26.2 {
/*return Minecraft.getInstance().gui.screen();
*///?} else
return Minecraft.getInstance().screen;
}
public static void set(@Nullable Screen screen) {
//? if >=26.2 {
/*Minecraft.getInstance().gui.setScreen(screen);
*///?} else
Minecraft.getInstance().setScreen(screen);
}
public static void clearOverlay() {
//? if >=26.2 {
/*Minecraft.getInstance().gui.setOverlay(null);
*///?} else
Minecraft.getInstance().setOverlay(null);
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.modules;
import org.saturnclient.ui2.RenderScope;
public interface HudMod {
public ModDimensions getDimensions();
public void renderDummy(RenderScope scope);
public void renderHud(RenderScope scope);
}
@@ -1,54 +0,0 @@
package org.saturnclient.modules;
import java.util.Map;
import org.saturnclient.saturnclient.config.NamedProperty;
import org.saturnclient.saturnclient.config.Property;
public class ModDimensions {
public Property<Integer> x = Property.integer(0);
public Property<Integer> y = Property.integer(0);
public Property<Float> scale = Property.floatProp(1.0f);
public Property<Integer> bgColor = Property.color(0x00000000);
public Property<Integer> fgColor = Property.color(0xFFffffff);
public Property<Integer> radius = Property.integer(0);
public Property<Integer> font = Property.font(0);
public boolean renderBackground = true;
public int width = 0;
public int height = 0;
public NamedProperty<Map<String, Property<?>>> prop() {
return Property.namespace(Map.of(
"X", x,
"Y", y,
"Scale", scale,
"Background Color", bgColor,
"Foreground Color", fgColor,
"Corner Radius", radius,
"Font", font
)).named("In-Game Display");
}
public ModDimensions() {}
public ModDimensions(int width, int height) {
this.width = width;
this.height = height;
}
public ModDimensions(int x, int y, int width, int height) {
this.x.value = x;
this.y.value = y;
this.width = width;
this.height = height;
}
public ModDimensions(int x, int y, int width, int height, float scale) {
this.x.value = x;
this.y.value = y;
this.scale.value = scale;
this.width = width;
this.height = height;
}
}
@@ -1,50 +0,0 @@
package org.saturnclient.modules;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.modules.mods.*;
public class ModManager {
public static final List<Module> ENABLED_MODS = new ArrayList<>();
public static final Module[] ALL_MODS = {
new Crosshair(),
new AutoSprint(),
new ArmorDisplay(),
new Fps(),
new PotionDisplay(),
new Coordinates(),
new Freelook(),
new Keystrokes()
};
public static void init() {
// Pre-filter enabled modules to avoid checking every tick
updateEnabledModules();
ClientTickEvents.END_CLIENT_TICK.register(client -> {
// Only iterate over enabled modules
for (Module m : ENABLED_MODS) {
m.tick();
}
});
}
public static void updateEnabledModules() {
ENABLED_MODS.clear();
for (Module m : ALL_MODS) {
if (m.isEnabled()) {
ENABLED_MODS.add(m);
}
}
}
public static Module[] getAllMods() {
return ALL_MODS;
}
public static void refreshEnabledModules() {
updateEnabledModules();
}
}
@@ -1,52 +0,0 @@
package org.saturnclient.modules;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.NamedProperty;
import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.util.Identifier;
public abstract class Module {
private final ModuleDetails details;
private final ConfigManager configManager;
public Module(ModuleDetails details, NamedProperty<?>... props) {
this.details = details;
configManager = new ConfigManager(details.name);
for (NamedProperty<?> prop : props) {
configManager.property(prop.name, prop.prop);
}
}
public void render(RenderScope scope) {}
public void tick() {}
public abstract boolean isEnabled();
public abstract void setEnabled(boolean e);
public final String getName() {
return details.name;
}
public Identifier getIconTexture() {
return Textures.getModIcon(details.namespace);
}
public ConfigManager getConfig() {
return configManager;
}
public String getDescription() {
return details.description;
}
public String[] getTags() {
return details.tags;
}
public String getVersion() {
return details.version;
}
}
@@ -1,39 +0,0 @@
package org.saturnclient.modules;
public class ModuleDetails {
public String name;
public String namespace;
public String description;
public String version;
public String[] tags = {};
public ModuleDetails(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
public ModuleDetails name(String name) {
this.name = name;
return this;
}
public ModuleDetails namespace(String namespace) {
this.namespace = namespace;
return this;
}
public ModuleDetails description(String description) {
this.description = description;
return this;
}
public ModuleDetails version(String version) {
this.version = version;
return this;
}
public ModuleDetails tags(String... tags) {
this.tags = tags;
return this;
}
}
@@ -1,6 +0,0 @@
# Credits
### Freelook
- https://github.com/Celibistrial/freelook
- https://modrinth.com/mod/freelook
**The other mods are coded by us, if you have a concern or we missed something feel free to submit a issue with the credits tag**
@@ -1,102 +0,0 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
public class ArmorDisplay extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 75);
private static Property<Boolean> useMainHand = Property.bool(false);
public ArmorDisplay() {
super(new ModuleDetails("Armor Display", "armor")
.description("Displays armor durability")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
useMainHand.named("Use Main Hand"),
dimensions.prop());
}
public void renderArmor(RenderScope scope, ItemStack mainHand, ItemStack helmet, ItemStack chestplate,
ItemStack leggings, ItemStack boots) {
int row = 0;
if (useMainHand.value && !mainHand.isEmpty()) {
scope.drawItem(mainHand, 0, 15 * row);
renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
row++;
}
if (!helmet.isEmpty()) {
scope.drawItem(helmet, 0, 15 * row);
renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
row++;
}
if (!chestplate.isEmpty()) {
scope.drawItem(chestplate, 0, 15 * row);
renderHealth(scope, row, chestplate.getMaxDamage(), chestplate.getDamage());
row++;
}
if (!leggings.isEmpty()) {
scope.drawItem(leggings, 0, 15 * row);
renderHealth(scope, row, leggings.getMaxDamage(), leggings.getDamage());
row++;
}
if (!boots.isEmpty()) {
scope.drawItem(boots, 0, 15 * row);
renderHealth(scope, row, boots.getMaxDamage(), boots.getDamage());
row++;
}
}
public void renderHealth(RenderScope scope, int i, int max_damage, int damage) {
if (max_damage > 0) {
scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value,
dimensions.fgColor.value);
}
}
@Override
public void renderHud(RenderScope scope) {
renderArmor(scope,
SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET));
}
@Override
public void renderDummy(RenderScope scope) {
renderArmor(scope, new ItemStack(Items.DIAMOND_SWORD), new ItemStack(Items.DIAMOND_HELMET),
new ItemStack(Items.DIAMOND_CHESTPLATE),
new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS));
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,42 +0,0 @@
package org.saturnclient.modules.mods;
import java.util.Objects;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
public class AutoSprint extends Module {
private static Property<Boolean> enabled = Property.bool(false);
public AutoSprint() {
super(
new ModuleDetails("Auto Sprint", "sprint")
.description("Makes the player always sprint")
.tags("Movement")
.version("v0.1.0"),
enabled.named("Enabled"));
}
@Override
public void tick() {
if (SaturnClient.client.player == null || SaturnClient.client.getNetworkHandler() == null) {
return;
}
if (SaturnClient.client.options.forwardKey.isPressed() && !SaturnClient.client.options.backKey.isPressed() && !SaturnClient.client.player.isSneaking() &&
!SaturnClient.client.player.horizontalCollision) {
Objects.requireNonNull(SaturnClient.client.player).setSprinting(true);
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,75 +0,0 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
public class Coordinates extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMethod = Property.select(0, "Flat", "Flat annotated", "Newline", "Newline annotated");
private static ModDimensions dimensions = new ModDimensions(40, 18);
public Coordinates() {
super(new ModuleDetails("Coordinates", "coords")
.description("Displays your current coordinates")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMethod.named("Display Method"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderCoords(532, 69, 253, scope);
}
@Override
public void renderHud(RenderScope scope) {
Vec3d pos = SaturnClient.client.player.getPos();
renderCoords((int) pos.x, (int) pos.y, (int) pos.z, scope);
}
public void renderCoords(int x, int y, int z, RenderScope scope) {
String f = "%d %d %d"; // Flat
switch (displayMethod.value) {
case 1: // Flat annotated
f = "X: %d Y: %d Z: %d";
break;
case 2: // Newline
f = "%d\n%d\n%d";
break;
case 3: // Newline annotated
f = "X: %d\nY: %d\nZ: %d";
break;
}
String text = String.format(f, x, y, z);
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,48 +0,0 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Textures;
import net.minecraft.client.render.RenderLayer;
public class Crosshair extends Module {
public static Property<Boolean> enabled = Property.bool(false);
public Crosshair() {
super(
new ModuleDetails("Crosshair", "crosshair")
.description("Changes the crosshair if you're aiming at a entity")
.tags("Visuals", "Utility")
.version("v0.1.0"),
enabled.named("Enabled"));
}
@Override
public void render(RenderScope scope) {
if (enabled.value
&& SaturnClient.client.targetedEntity != null
&& SaturnClient.client.targetedEntity.isAlive()) {
int scaledWidth = 15;
int scaledHeight = 15;
scope.setRenderLayer(RenderLayer::getCrosshair);
scope.drawTexture(Textures.CROSSHAIR_RANGE,
(scope.getScaledWindowWidth() - scaledWidth) / 2,
(scope.getScaledWindowHeight() - scaledHeight) / 2, 0.0F, 0.0F, scaledWidth, scaledHeight);
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,53 +0,0 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class Fps extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
public Fps() {
super(new ModuleDetails("FPS Display", "fps")
.description("Displays current FPS")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
@Override
public void renderHud(RenderScope scope) {
String text = String.valueOf(SaturnClient.client.getCurrentFps()) + " FPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public void renderDummy(RenderScope scope) {
String text = "369 FPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
}
@@ -1,71 +0,0 @@
package org.saturnclient.modules.mods;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import net.minecraft.client.option.Perspective;
public class Freelook extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(true);
public static Property<Integer> freelookKey = Property.keybinding(GLFW.GLFW_KEY_H);
public static boolean isFreeLooking = false;
private static boolean was1stPerson = false;
public Freelook() {
super(
new ModuleDetails("Freelook", "freelook")
.description("Allows you to look around freely without moving your character")
.tags("Movement", "Camera")
.version("0.1.0"),
enabled.named("Enabled"),
toggle.named("Toggle freelook"),
freelookKey.named("Freelook Keybinding")
);
}
@Override
public void tick() {
if (toggle.value) {
if (freelookKey.wasKeyPressed()) {
if (isFreeLooking) {
stopFreelook();
} else {
startFreelook();
}
}
} else if (freelookKey.isKeyPressed()) {
if (!isFreeLooking) {
startFreelook();
}
} else if (isFreeLooking) {
stopFreelook();
}
}
private void startFreelook() {
isFreeLooking = true;
was1stPerson = SaturnClient.client.options.getPerspective() == Perspective.FIRST_PERSON;
SaturnClient.client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
private void stopFreelook() {
if (was1stPerson) {
SaturnClient.client.options.setPerspective(Perspective.FIRST_PERSON);
}
isFreeLooking = false;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -1,174 +0,0 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.modules.ModuleDetails;
public class Keystrokes extends Module implements HudMod {
public static Property<Boolean> enabled = Property.bool(false);
public static Property<Boolean> showMouse = Property.bool(true);
public static Property<Boolean> showSpace = Property.bool(false);
public static ModDimensions dimensions = new ModDimensions(78, 46);
// Cache key states to avoid repeated calls
private boolean w = false;
private boolean a = false;
private boolean s = false;
private boolean d = false;
private boolean lmb = false;
private boolean rmb = false;
private boolean space = false;
// Cache dimensions to avoid recalculation
private int cachedHeight = 54;
private boolean dimensionsDirty = true;
private static Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static Property<Integer> clickFg = Property.color(0xFFFFFFFF);
public Keystrokes() {
super(new ModuleDetails("Keystrokes", "keystrokes")
.description("Displays the keystrokes for movement")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
showMouse.named("Show mouse clicks"),
showSpace.named("Show space clicks"),
dimensions.prop(),
clickFg.named("Clicked fg"),
clickBg.named("Clicked bg"));
dimensions.renderBackground = false;
}
@Override
public void tick() {
// Cache key states to reduce method calls
w = SaturnClient.client.options.forwardKey.isPressed();
a = SaturnClient.client.options.leftKey.isPressed();
s = SaturnClient.client.options.backKey.isPressed();
d = SaturnClient.client.options.rightKey.isPressed();
lmb = SaturnClient.client.options.attackKey.isPressed();
rmb = SaturnClient.client.options.useKey.isPressed();
space = SaturnClient.client.options.jumpKey.isPressed();
// Mark dimensions as dirty if mouse or space visibility changed
if (dimensionsDirty) {
updateDimensions();
dimensionsDirty = false;
}
}
private void updateDimensions() {
cachedHeight = 54; // base height with padding considered
if (showMouse.value) {
cachedHeight += 27;
}
if (showSpace.value) {
cachedHeight += 22;
}
dimensions.height = cachedHeight;
}
@Override
public void renderDummy(RenderScope scope) {
// Use cached dimensions
dimensions.height = cachedHeight;
// WASD keys (3px padding between each key)
renderKey(scope, false, 'W', 27, 0); // Top center
renderKey(scope, false, 'A', 0, 27); // Bottom left
renderKey(scope, false, 'S', 27, 27); // Bottom center
renderKey(scope, false, 'D', 54, 27); // Bottom right
if (showMouse.value) {
renderKeyM(scope, false, "LMB", 0, 54); // Below A/S/D
renderKeyM(scope, false, "RMB", 40, 54); // With 3px between
}
if (showSpace.value) {
renderKeySpace(scope, false, 0, cachedHeight - (showSpace.value ? 22 : 0));
}
}
@Override
public void renderHud(RenderScope scope) {
// Use cached dimensions
dimensions.height = cachedHeight;
// WASD keys (3px padding between each key)
renderKey(scope, w, 'W', 27, 0); // Top center
renderKey(scope, a, 'A', 0, 27); // Bottom left
renderKey(scope, s, 'S', 27, 27); // Bottom center
renderKey(scope, d, 'D', 54, 27); // Bottom right
if (showMouse.value) {
renderKeyM(scope, lmb, "LMB", 0, 54); // Below A/S/D
renderKeyM(scope, rmb, "RMB", 40, 54); // With 3px between
}
if (showSpace.value) {
renderKeySpace(scope, space, 0, cachedHeight - (showSpace.value ? 22 : 0));
}
}
// Consistent 3px padding applied below:
private void renderKeySpace(RenderScope scope, boolean isPressed, int x, int y) {
int height = 19;
int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, dimensions.width, height, dimensions.radius.value, bgColor);
// A 30-pixel line centered vertically with 3px top padding
int lineY = y + (height - 1) / 2;
scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, fgColor);
}
private void renderKeyM(RenderScope scope, boolean isPressed, String c, int x, int y) {
int width = 38, height = 24;
int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, width, height, dimensions.radius.value, bgColor);
// Apply padding: 3px top and left -> add slight offset to x/y
scope.drawText(0.6f, c, x + 9, y + 7, dimensions.font.value, fgColor);
}
private void renderKey(RenderScope scope, boolean isPressed, char c, int x, int y) {
int size = 24;
int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, size, size, dimensions.radius.value, bgColor);
// Center the character with 3px top padding
scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, fgColor);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
dimensionsDirty = true;
}
}
@@ -1,105 +0,0 @@
package org.saturnclient.modules.mods;
import java.util.Arrays;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.texture.StatusEffectSpriteManager;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
public class PotionDisplay extends Module implements HudMod {
public static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, 0);
private final StatusEffectInstance[] dummyEffects = {
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2),
};
public PotionDisplay() {
super(new ModuleDetails("Potion Display", "potion")
.description("Displays potion effects")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
private void renderEffects(RenderScope scope, Iterable<StatusEffectInstance> effects) {
int row = 0;
StatusEffectSpriteManager statusEffectSpriteManager = SaturnClient.client.getStatusEffectSpriteManager();
for (StatusEffectInstance effect : effects) {
if (effect.shouldShowIcon()) {
RegistryEntry<StatusEffect> registryEntry = effect.getEffectType();
Sprite sprite = statusEffectSpriteManager.getSprite(registryEntry);
scope.drawSpriteStretched(sprite, 0, 18 * row, 16, 16);
scope.drawText(0.5f, getDurationAsString(effect), 18, 18 * row + 3, dimensions.font.value, -1);
row++;
}
}
dimensions.height = 18 * row;
}
@Override
public void renderHud(RenderScope scope) {
renderEffects(scope, SaturnClient.client.player.getStatusEffects());
}
@Override
public void renderDummy(RenderScope scope) {
renderEffects(scope, Arrays.asList(dummyEffects));
}
// This code was from: https://github.com/magicus/statuseffecttimer/blob/master/src/main/java/se/icus/mag/statuseffecttimer/mixin/StatusEffectTimerMixin.java
private String getDurationAsString(StatusEffectInstance statusEffectInstance) {
if (statusEffectInstance.isInfinite()) {
return I18n.translate("effect.duration.infinite");
}
int ticks = MathHelper.floor((float) statusEffectInstance.getDuration());
int totalSeconds = ticks / 20;
if (totalSeconds >= 3600) {
return totalSeconds / 3600 + "h";
} else if (totalSeconds >= 60) {
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return String.format("%d:%02d", minutes, seconds);
} else {
return String.valueOf(totalSeconds) + "s";
}
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void setEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,44 +0,0 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
import org.saturnclient.modules.mods.Freelook;
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
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;
@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 (Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && MinecraftClient.getInstance().player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(MinecraftClient.getInstance().player.getPitch());
cameraOverriddenEntity.freelook$setCameraYaw(MinecraftClient.getInstance().player.getYaw());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(), cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -1,62 +0,0 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
import org.saturnclient.modules.mods.Freelook;
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
//noinspection ConstantValue// IntelliJ is incorrect here, this code block is reachable
if (Freelook.isFreeLooking && (Object) this instanceof ClientPlayerEntity) {
double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15);
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta;
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return this.cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return this.cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float pitch) {
this.cameraPitch = pitch;
}
@Override
@Unique
public void freelook$setCameraYaw(float yaw) {
this.cameraYaw = yaw;
}
}
@@ -1,58 +0,0 @@
package org.saturnclient.modules.mods.mixins;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderTickCounter;
@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;
if (textRenderer != null &&
!(SaturnClient.client.currentScreen instanceof HudEditor)) {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (org.saturnclient.modules.Module m : ModManager.ENABLED_MODS) {
if (m instanceof HudMod && m.isEnabled()) {
ModDimensions dim = ((HudMod) m).getDimensions();
renderScope.matrices.push();
renderScope.matrices.translate(dim.x.value, (float) dim.y.value, 0);
renderScope.matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value,
dim.bgColor.value);
}
renderScope.setRenderLayer(RenderLayer::getGuiTextured);
((HudMod) m).renderHud(renderScope);
renderScope.matrices.pop();
} else {
m.render(renderScope);
}
}
}
}
}
@@ -1,9 +0,0 @@
package org.saturnclient.modules.mods.utils;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -2,28 +2,26 @@ 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.screen.v1.ScreenEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.util.math.MatrixStack;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.Minecraft;
import org.saturnclient.modules.ModManager;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.cosmetics.Emotes;
import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.cosmetics.Emotes;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.impl.provider.GLFWProviderImpl;
import org.saturnclient.impl.provider.FeatureProviderFabric;
import org.saturnclient.impl.provider.RefConstructorImpl;
import org.saturnclient.impl.provider.SaturnProviderImpl;
import org.saturnclient.impl.ui.EntityDrawerImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.mod.ModManager;
import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.ui2.screens.TitleMenu;
import org.saturnclient.config.Config;
import org.saturnclient.config.ConfigManager;
import org.saturnclient.ui.components.SkinPreview;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.felnull.specialmodelloader.api.event.SpecialModelLoaderEvents;
/**
* Main class for the Saturn Client mod.
*/
@@ -31,56 +29,36 @@ 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;
public static Minecraft client;
@Override
public void onInitialize() {
LOGGER.info("Initializing " + MOD_ID);
client = MinecraftClient.getInstance();
SaturnClientConfig.init();
client = Minecraft.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());
SpecialModelLoaderEvents.LOAD_SCOPE.register(() -> {
return (resourceManager, location) -> MOD_ID.equals(location.getNamespace());
});
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
if (screen != null && !(screen instanceof TitleMenu)) {
if (screen instanceof SaturnScreen) {
((SaturnScreen) screen).draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18)
.position(screen.width - 180 - 20, screen.height - 18 - 20));
} else {
ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, tickDelta) -> {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
int width = context.getScaledWindowWidth();
int height = context.getScaledWindowHeight();
float scaleX = (float) screen1.width / width;
float scaleY = (float) screen1.height / height;
MatrixStack matrixStack = context.getMatrices();
matrixStack.push();
matrixStack.translate(width - 95, height - 14, 0);
matrixStack.scale(0.5f / scaleX, 0.5f / scaleY, 1.0f);
renderScope.drawTexture(Textures.LOGO_TEXT_BIG, 0, 0, 0, 0, 180, 18);
matrixStack.pop();
});
}
ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (org.saturnclient.mod.Mod m : ModManager.ENABLED_MODS) {
m.tick();
}
});
KeyInputHandler.register();
Emotes.initialize();
if (Auth.authenticate()) {
Cloaks.initialize();
Hats.initialize();
LOGGER.info(MOD_ID + " initialization complete");
} else {
LOGGER.error("Failed to authenticate with the server");
}
ServiceClient.initialize();
}
}
@@ -1,51 +0,0 @@
package org.saturnclient.saturnclient;
import org.saturnclient.saturnclient.config.Property;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.ui2.resources.Textures;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
public class SaturnClientConfig {
public static ConfigManager config;
public static Property<Boolean> realisticLogo = Property.bool(false);
public static Property<Boolean> saturnTitleScreen = Property.bool(true);
public static Property<Boolean> bendyCloaks = Property.bool(true);
public static Property<Integer> openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B);
public static Identifier getLogo() {
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
}
public static String getSaturnIndicator() {
return realisticLogo.value ? "" : "";
}
/*
* Gets the icon color of a specific individual, here are the different colors
*
* - Owner: Dark Red
* - Admin: Red
* - Partners: Gold
* - Contributor: Aqua
* - Other/player: White
*/
public static Formatting getIconColor(String uuid) {
if (uuid.equals("d362a04228bd49e1a807ae74dbe8aba9")) {
return Formatting.DARK_RED; // Owner
}
return Formatting.WHITE;
}
public static void init() {
config = new ConfigManager("Saturn Client");
config.property("Realistic logo", realisticLogo);
config.property("Saturn client title screen", saturnTitleScreen);
config.property("Open Emote Wheel", openEmoteWheel);
config.property("Bendy Cloaks", bendyCloaks);
}
}
@@ -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");
*///?}
}
@@ -1,283 +0,0 @@
package org.saturnclient.saturnclient.auth;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.util.Identifier;
import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.elements.Notification.NotificationKind;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import org.saturnclient.saturnclient.SaturnClient;
public class Auth {
public static Map<String, SaturnPlayer> players = new HashMap<>();
public static Map<String, String> playerNames = new HashMap<>();
public static String uuid;
private static Thread listenerThread;
private static Thread pingThread;
private static volatile boolean running = false;
public static boolean authenticate() {
// Register shutdown hook ONCE
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close());
try {
if (SaturnClient.client.getSession() == null) {
SaturnClient.LOGGER.error("No active Minecraft session found");
return false;
}
String accessToken = SaturnClient.client.getSession().getAccessToken();
SaturnClient.LOGGER.info("Authenticating");
Network.init();
Network.write(accessToken);
String response = Network.read();
SaturnParser parser = new SaturnParser(response);
uuid = parser.getString("uuid");
String cloak = parser.getString("cloak");
String hat = parser.getString("hat");
for (String availableCloak : parser.getArray("cloaks")) {
Cloaks.availableCloaks.add(availableCloak);
}
for (String availableHat : parser.getArray("hats")) {
Hats.availableHats.add(availableHat);
}
players.put(uuid, new SaturnPlayer(cloak, hat));
Cloaks.loadCloak(uuid);
playerNames.put(SaturnClient.client.getSession().getUsername(), uuid);
afterAuth();
startListenerThread();
startPingThread();
return true;
} catch (Exception e) {
SaturnClient.LOGGER.error("Authentication failed", e);
return false;
}
}
private static void startListenerThread() {
running = true;
listenerThread = new Thread(() -> {
try {
while (running) {
String message = Network.read();
if (message == null) {
SaturnClient.LOGGER.error("Connection closed");
running = false;
break;
}
SaturnClient.LOGGER.info("Received: " + message);
SaturnParser parser = new SaturnParser(message);
switch (parser.method) {
case "player":
String uuid = parser.getString("uuid");
if (parser.getBool("saturn")) {
String cloak = parser.getString("cloak");
String hat = parser.getString("hat");
players.put(uuid, new SaturnPlayer(cloak, hat));
Cloaks.loadCloak(uuid);
} else {
playerNames.entrySet().removeIf(entry -> entry.getValue().equals(uuid));
}
break;
case "successful_transaction":
String cloak = parser.getStringOrNull("cloak");
String hat = parser.getStringOrNull("hat");
if (cloak != null) {
Cloaks.availableCloaks.add(cloak);
}
if (hat != null) {
Hats.availableHats.add(hat);
}
case "update_cloak":
String targetUuid = parser.getStringOrNull("uuid");
String targetCloak = parser.getStringOrNull("cloak");
if (targetUuid != null && targetCloak != null) {
SaturnPlayer player = players.get(targetUuid);
if (player != null) {
player.cloak = targetCloak;
}
}
break;
case "update_hat":
targetUuid = parser.getStringOrNull("uuid");
String targetHat = parser.getStringOrNull("hat");
if (targetUuid != null && targetHat != null) {
SaturnPlayer player = players.get(targetUuid);
if (player != null) {
player.hat = targetHat;
}
}
break;
case "emote":
targetUuid = parser.getStringOrNull("uuid");
String emote = parser.getStringOrNull("name");
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
if (player.getUuidAsString().replace("-", "").equals(targetUuid)) {
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
if (emote != null && !emote.isEmpty()) {
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
animationStack.removeLayer(1000);
}
animationStack.addAnimLayer(1000,
PlayerAnimationRegistry
.getAnimation(Identifier.of("saturnclient", emote))
.playAnimation());
} else {
animationStack.removeLayer(1000);
}
}
}
break;
default:
if (parser.error != null) {
String[] a = parser.error.replace("!", "").split(": ");
Utils.notify(NotificationKind.Error, a[0], a[1]);
}
break;
}
}
} catch (Exception e) {
SaturnClient.LOGGER.error("Error reading from server", e);
} finally {
close();
}
});
listenerThread.setDaemon(true);
listenerThread.start();
}
private static void startPingThread() {
pingThread = new Thread(() -> {
try {
while (running && !Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(25000); // 25 seconds
if (running) {
Network.write("ping");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore interrupt status
break; // Exit the loop on interruption
}
}
} catch (Exception e) {
if (!(e instanceof InterruptedException)) {
SaturnClient.LOGGER.error("Error in ping thread", e);
}
}
});
pingThread.setDaemon(true);
pingThread.start();
}
public static void close() {
running = false;
if (pingThread != null) {
pingThread.interrupt();
}
if (listenerThread != null) {
listenerThread.interrupt();
}
try {
close();
SaturnClient.LOGGER.info("SaturnApi connection closed.");
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to close resources", e);
}
}
public static void afterAuth() {
PlayerTracker.initialize();
}
public static void setCloak(String cloakName) {
try {
Network.write("set_cloak@cloak=" + cloakName + "@notify=" + stringifyPlayers());
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void setHat(String hatName) {
try {
Network.write("set_hat@hat=" + hatName + "@notify=" + stringifyPlayers());
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void emote(String name) {
try {
Network.write("emote@name=" + name + "@notify=" + stringifyPlayers());
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static String stringifyPlayers() {
return players.keySet().stream()
.filter(key -> key != uuid)
.collect(Collectors.joining("$"));
}
public static void player(String name, String uuid) {
try {
playerNames.put(name, uuid.replaceAll("-", ""));
Network.write("player@uuid=" + uuid.replaceAll("-", ""));
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void buyCloak(String name) {
try {
Network.write("buy_cloak@cloak=" + name);
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void buyHat(String name) {
try {
Network.write("buy_hat@hat=" + name);
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
}
@@ -1,98 +0,0 @@
package org.saturnclient.saturnclient.auth;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.Base64;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.interfaces.*;
import java.security.spec.X509EncodedKeySpec;
public class Network {
static Socket socket;
static PrintWriter out;
static BufferedReader in;
static byte[] key = new byte[32];
public static class AES {
public static byte[] encryptAES256(String message, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encryptedMessage = cipher.doFinal(message.getBytes());
return encryptedMessage;
}
public static String decryptAES256(byte[] encryptedMessage, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedMessage = cipher.doFinal(encryptedMessage);
return new String(decryptedMessage);
}
}
public static void init() throws Exception {
// Generate AES key
new SecureRandom().nextBytes(key);
socket = new Socket("77.247.92.168", 8080);
// out = new DataOutputStream(socket.getOutputStream());
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
StringBuilder publicKeyBuilder = new StringBuilder();
String line;
while (!(line = in.readLine()).contains("-----END PUBLIC KEY-----")) {
publicKeyBuilder.append(line).append("\n");
}
publicKeyBuilder.append(line);
String publicKeyPem = publicKeyBuilder.toString();
String publicKeyBase64 = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "").replace("\n", "").replace("\r", "");
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedKey = rsaCipher.doFinal(key);
out.println(Base64.getEncoder().encodeToString(encryptedKey));
}
public static void write(String message) {
try {
out.println(Base64.getEncoder().encodeToString(AES.encryptAES256(message, key)));
} catch (Exception e) {
throw new RuntimeException("Failed to write to server: " + e.getMessage());
}
}
public static String read() {
try {
String msg = in.readLine();
if (msg != null) {
return AES.decryptAES256(Base64.getDecoder().decode(msg), key);
} else {
return "";
}
} catch (Exception e) {
return "";
}
}
public static void close() {
try {
socket.close();
out.close();
in.close();
} catch (Exception e) {
}
}
}
@@ -1,52 +0,0 @@
package org.saturnclient.saturnclient.auth;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.client.MinecraftClient;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import com.mojang.authlib.GameProfile;
public class PlayerTracker {
private static Set<UUID> trackedPlayers = new HashSet<>();
public static void initialize() {
// When client joins a server, reset tracked players
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
trackedPlayers.clear();
client.execute(() -> {
checkForNewPlayers(SaturnClient.client);
});
});
// Check for new players every tick
ClientTickEvents.END_CLIENT_TICK.register(client -> checkForNewPlayers(client));
}
private static void checkForNewPlayers(MinecraftClient client) {
if (client.getNetworkHandler() == null)
return;
for (PlayerListEntry player : client.getNetworkHandler().getPlayerList()) {
UUID playerUUID = player.getProfile().getId();
if (!trackedPlayers.contains(playerUUID)) {
// A new player has joined
onPlayerJoin(client, player);
trackedPlayers.add(playerUUID);
}
}
}
private static void onPlayerJoin(MinecraftClient client, PlayerListEntry player) {
GameProfile profile = player.getProfile();
String uuid = profile.getId().toString();
if (uuid != Auth.uuid)
Auth.player(profile.getName(), uuid);
}
}
@@ -1,71 +0,0 @@
package org.saturnclient.saturnclient.auth;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class SaturnParser {
public String method = "";
public String error;
private Map<String, String> params = new HashMap<>();
public SaturnParser(String input) throws IOException {
String[] parts = input.split("@");
if (parts.length == 0) {
throw new IOException("Invalid input string");
}
if (parts[0].startsWith("!")) {
error = input;
}
method = parts[0];
for (int i = 1; i < parts.length; i++) {
String[] keyValue = parts[i].split("=");
if (keyValue.length == 2) {
params.put(keyValue[0].trim(), keyValue[1].trim());
} else if (keyValue.length == 1) {
params.put(keyValue[0].trim(), "");
} else {
throw new IOException("Invalid parameter format: " + parts[i]);
}
}
}
public String getString(String i) throws IOException {
String val = params.get(i);
if (val != null) {
return val;
} else {
throw new IOException("Parameter not found " + i);
}
}
public String getStringOrNull(String i) throws IOException {
String val = params.get(i);
return val;
}
public boolean getBool(String i) throws IOException {
String val = params.get(i);
if (val != null) {
return val.equals("true");
} else {
throw new IOException("Parameter not found " + i);
}
}
public String[] getArray(String i) throws IOException {
String val = params.get(i);
if (val != null) {
if (val.isEmpty()) {
return new String[] {};
}
return val.split("\\$");
} else {
throw new IOException("Parameter not found " + i);
}
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.saturnclient.auth;
public class SaturnPlayer {
public String cloak = "";
public String hat = "";
public SaturnPlayer(String cloak, String hat) {
this.cloak = cloak;
this.hat = hat;
}
}
@@ -1,119 +0,0 @@
package org.saturnclient.saturnclient.config;
import com.google.gson.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import org.saturnclient.saturnclient.SaturnClient;
public class ConfigManager {
private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json");
private static final Map<String, Map<String, Property<?>>> properties = new HashMap<>();
private static JsonObject cachedThemeJson = null;
private final Map<String, Property<?>> currentMap;
private final String namespace;
public ConfigManager(String namespace) {
this.namespace = namespace;
this.currentMap = new LinkedHashMap<>();
properties.put(namespace, currentMap);
System.out.println("Created namespace: " + namespace);
}
public ConfigManager(ConfigManager parent, String namespace) {
this.namespace = namespace;
this.currentMap = new LinkedHashMap<>();
Property<Map<String, Property<?>>> nsProp = Property.namespace(currentMap);
parent.property(namespace, nsProp);
System.out.println("Created sub-namespace: " + namespace);
}
public void sub(String name) {
property(name, Property.namespace(new LinkedHashMap<>()));
}
public <T> Property<T> property(String name, Property<T> value) {
currentMap.put(name, value);
loadProp(name, value);
return value;
}
private <T> void loadProp(String name, Property<T> prop) {
JsonObject source = cachedThemeJson != null ? cachedThemeJson : loadAndCache();
if (source == null)
return;
JsonElement namespaceJson = source.get(namespace);
if (namespaceJson == null || !namespaceJson.isJsonObject())
return;
JsonElement value = namespaceJson.getAsJsonObject().get(name);
if (value != null)
prop.loadFromJson(value);
}
private static JsonObject loadAndCache() {
try {
if (!configFile.exists()) {
Files.writeString(configFile.toPath(), "{}");
return new JsonObject();
}
String json = Files.readString(configFile.toPath());
cachedThemeJson = JsonParser.parseString(json).getAsJsonObject();
return cachedThemeJson;
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading config file", e);
return null;
}
}
public static void load() {
SaturnClient.LOGGER.info("Loading config...");
JsonObject json = loadAndCache();
if (json == null)
return;
properties.forEach((namespace, map) -> {
JsonElement nsElement = json.get(namespace);
if (nsElement != null && nsElement.isJsonObject()) {
loadProperties(nsElement.getAsJsonObject(), map);
}
});
}
private static void loadProperties(JsonObject json, Map<String, Property<?>> props) {
props.forEach((name, prop) -> {
JsonElement el = json.get(name);
if (el != null)
prop.loadFromJson(el);
});
}
public static void save() {
SaturnClient.LOGGER.info("Saving config...");
JsonObject root = new JsonObject();
properties.forEach((namespace, map) -> {
JsonObject nsJson = new JsonObject();
saveProperties(nsJson, map);
root.add(namespace, nsJson);
});
try {
String formatted = new GsonBuilder().setPrettyPrinting().create().toJson(root);
Files.writeString(configFile.toPath(), formatted);
SaturnClient.LOGGER.info("Config saved successfully.");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error saving config file", e);
}
}
private static void saveProperties(JsonObject json, Map<String, Property<?>> props) {
props.forEach((name, prop) -> json.add(name, prop.toJson()));
}
public Map<String, Property<?>> getProperties() {
return currentMap;
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.saturnclient.config;
public class NamedProperty<T> {
public String name;
public Property<T> prop;
public NamedProperty(String name, Property<T> value) {
this.name = name;
this.prop = value;
}
}
@@ -1,280 +0,0 @@
package org.saturnclient.saturnclient.config;
import java.util.Map;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
public class Property<T> {
public enum PropertyType {
BOOLEAN,
INTEGER,
FLOAT,
STRING,
HEX,
NAMESPACE,
SELECT,
KEYBINDING,
}
public T value;
public boolean isReset = false;
private T defaultValue;
private String[] availableValues;
private final PropertyType type;
private boolean wasPressedLastTick = false;
private Property(T value, PropertyType type) {
this.value = value;
this.defaultValue = value;
this.type = type;
}
// ---------- Factory Methods ----------
public static <T> Property<T> from(T value) {
if (value instanceof Boolean)
return new Property<>(value, PropertyType.BOOLEAN);
if (value instanceof Integer)
return new Property<>(value, PropertyType.INTEGER);
if (value instanceof Float)
return new Property<>(value, PropertyType.FLOAT);
if (value instanceof String)
return new Property<>(value, PropertyType.STRING);
if (valueIsNamespace(value))
return new Property<>(value, PropertyType.NAMESPACE);
return null;
}
public static Property<Integer> font(int value) {
return select(value, "Default", "Inter", "Inter bold");
}
public static Property<Integer> select(Integer value, String... availableValues) {
Property<Integer> property = new Property<>(value, PropertyType.SELECT);
property.availableValues = availableValues;
return property;
}
public static Property<Map<String, Property<?>>> namespace(Map<String, Property<?>> value) {
return new Property<>(value, PropertyType.NAMESPACE);
}
public static Property<Boolean> bool(boolean value) {
return new Property<>(value, PropertyType.BOOLEAN);
}
public static Property<Integer> integer(int value) {
return new Property<>(value, PropertyType.INTEGER);
}
public static Property<Float> floatProp(float value) {
return new Property<>(value, PropertyType.FLOAT);
}
public static Property<String> string(String value) {
return new Property<>(value, PropertyType.STRING);
}
public static Property<Integer> color(int value) {
return new Property<>(value, PropertyType.HEX);
}
public static Property<Integer> keybinding(int value) {
return new Property<>(value, PropertyType.KEYBINDING);
}
// ---------- Select Helpers ----------
public void next() {
if (type == PropertyType.SELECT) {
int i = (Integer) value;
setValue((i < availableValues.length - 1) ? i + 1 : 0);
}
}
public void prev() {
if (type == PropertyType.SELECT) {
int i = (Integer) value;
setValue((i > 0) ? i - 1 : availableValues.length - 1);
}
}
public void setSelection(int selection) {
if (type == PropertyType.SELECT && selection >= 0 && selection < availableValues.length) {
setValue(selection);
}
}
public String getSelection() {
return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null;
}
// ---------- Lifecycle ----------
public void reset() {
value = defaultValue;
isReset = true;
}
public PropertyType getType() {
return type;
}
@SuppressWarnings("unchecked")
public Map<String, Property<?>> getNamespaceValue() {
if (type == PropertyType.NAMESPACE && value instanceof Map) {
return (Map<String, Property<?>>) value;
}
throw new IllegalStateException("Property is not a namespace");
}
// ---------- JSON Serialization ----------
/*
* BOOLEAN,
* INTEGER,
* FLOAT,
* STRING,
* HEX,
* NAMESPACE,
* SELECT,
* KEYBINDING,
*/
public JsonElement toJson() {
switch (type) {
case BOOLEAN:
return new JsonPrimitive((Boolean) value);
case INTEGER:
return new JsonPrimitive((Integer) value);
case FLOAT:
return new JsonPrimitive((Float) value);
case NAMESPACE:
JsonObject nested = new JsonObject();
getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson()));
return nested;
case HEX:
return new JsonPrimitive(String.format("#%08X", (Integer) value));
case KEYBINDING:
return new JsonPrimitive((Integer) value);
case SELECT:
return new JsonPrimitive((Integer) value);
default:
return new JsonPrimitive(String.valueOf(value));
}
}
public void loadFromJson(JsonElement element) {
if (element == null)
return;
switch (type) {
case BOOLEAN:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean())
setValue(element.getAsBoolean());
break;
case INTEGER:
if (element.isJsonPrimitive()) {
JsonPrimitive p = element.getAsJsonPrimitive();
if (p.isNumber())
setValue(p.getAsInt());
else if (p.isString())
setValue(parseHexToInt(p.getAsString()));
}
break;
case FLOAT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsFloat());
break;
case STRING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(element.getAsString());
break;
case HEX:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(parseHexToInt(element.getAsString()));
break;
case NAMESPACE:
if (element.isJsonObject()) {
JsonObject obj = element.getAsJsonObject();
getNamespaceValue().forEach((k, v) -> v.loadFromJson(obj.get(k)));
}
break;
case SELECT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setSelection(element.getAsInt());
break;
case KEYBINDING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsInt());
break;
}
}
// ---------- Utility ----------
@Override
public String toString() {
return String.valueOf(value);
}
public static boolean valueIsNamespace(Object obj) {
return obj instanceof Map;
}
@SuppressWarnings("unchecked")
public void setValue(Object value) {
this.value = (T) value;
}
@SuppressWarnings("unchecked")
public void setValue(Object value, Object defaultValue) {
this.value = (T) value;
this.defaultValue = (T) defaultValue;
}
public NamedProperty<T> named(String name) {
return new NamedProperty<>(name, this);
}
// ---------- Keybindings ----------
public boolean isKeyPressed() {
return (Integer) value != -1 &&
GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS &&
SaturnClient.client.currentScreen == null;
}
public boolean wasKeyPressed() {
boolean pressed = isKeyPressed();
boolean result = pressed && !wasPressedLastTick;
wasPressedLastTick = pressed;
return result;
}
// ---------- HEX ----------
public static int parseHexToInt(String hex) {
hex = hex.replace("#", "");
if (hex.length() == 6)
hex = "FF" + hex;
if (hex.length() != 8)
throw new IllegalArgumentException("Hex must be 6 or 8 chars long, got \'" + hex + "\'");
return (int) Long.parseLong(hex, 16);
}
// Misc
public Property<T> copy() {
return new Property<>(this.value, this.type);
}
public void load(String name, JsonElement element) {
if (element != null && element.isJsonObject()) {
load(name, element.getAsJsonObject());
}
}
public void load(String name, JsonObject element) {
JsonElement value = element.get(name);
if (value != null)
this.loadFromJson(value);
}
}
@@ -1,93 +0,0 @@
package org.saturnclient.saturnclient.config;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class ThemeManager {
private static File themeFile = new File(
SaturnClient.client.runDirectory,
"saturn.theme.json");
private static List<ThemeManager> theme = new ArrayList<>();
private static JsonObject cachedThemeJson = null;
private Map<String, Map<String, Object>> states;
private Map<String, Property<?>> currentStyling;
private String namespace;
public ThemeManager(String namespace, String... states) {
this.namespace = namespace;
this.states = new LinkedHashMap<>();
this.currentStyling = new LinkedHashMap<>();
this.states.put(null, new LinkedHashMap<>());
for (String s : states) {
this.states.put(s, new LinkedHashMap<>());
}
theme.add(this);
}
public void setState(String state) {
for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
}
}
public void applyState(String state) {
for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
if (!states.get(null).get(tProp.getKey()).equals(tProp.getValue()))
currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
}
}
public <T> Property<T> property(String name, Property<T> prop) {
for (Map.Entry<String, Map<String, Object>> state : this.states.entrySet()) {
Property<?> property = prop.copy();
if (cachedThemeJson == null) {
load();
}
String stateName = state.getKey();
property.load(name, cachedThemeJson.get(stateName != null ? namespace + "@" + stateName : namespace));
state.getValue().put(name, property.value);
}
currentStyling.put(name, prop);
return prop;
}
public <T> void propertyStateDefault(String state, String name, T value) {
Property<?> prop = currentStyling.get(name).copy();
prop.setValue(value, value);
if (cachedThemeJson == null) {
load();
}
prop.load(name, cachedThemeJson.get(state != null ? namespace + "@" + state : namespace));
states.get(state).put(name, prop.value);
}
public static void load() {
try {
SaturnClient.LOGGER.info("Starting to load theme...");
if (!themeFile.exists()) {
themeFile.createNewFile();
Files.write(themeFile.toPath(), "{}".getBytes());
}
cachedThemeJson = JsonParser.parseString(
new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject();
SaturnClient.LOGGER.info("Theme loaded");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the theme file", e);
}
}
}
@@ -1,353 +0,0 @@
package org.saturnclient.saturnclient.cosmetics;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.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.Identifier;
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 = 16;
private float currentVelocity = 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;
}
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 float getCurve(int i, float curveMagnitude, PlayerEntityRenderState playerState) {
float powCurve = (float) Math.pow((double) (PARTS - i) / PARTS, 2) * curveMagnitude;
float waveFreq = 0.6f;
float phase = (playerState.age + playerState.handSwingProgress) * 0.4f;
float sineAmplitude = 0.02f;
float sineCurve = (float) Math.sin(i * waveFreq + phase) * sineAmplitude;
float t = Math.min(1.0f, Math.max(0.0f, (curveMagnitude - 0.3f) / 0.2f));
t = t * t * (3 - 2 * t);
return (1.0f - t) * powCurve + t * (powCurve + sineCurve);
}
private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer,
PlayerEntityRenderState playerState, int light, int overlay,
float rotation, float curveMagnitude) {
float capeWidth = 10.0f / 16.0f;
float capeHeight = 16.0f / 16.0f;
float capeDepth = 1.0f / 16.0f;
float capePartHeight = capeHeight / PARTS;
matrixStack.push();
matrixStack.translate(0.0f, 1.25, 0.125f);
matrixStack.translate(0, -capeHeight, 0);
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(rotation));
matrixStack.translate(0, capeHeight, 0);
float x1 = -capeWidth / 2.0f;
float x2 = capeWidth / 2.0f;
float y2 = -capeHeight;
float z1 = 0.0f;
float z2 = capeDepth;
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();
for (int i = 0; i < PARTS; i++) {
float y3 = -(capePartHeight * i);
float y4 = -(capePartHeight * (i + 1));
float curveOffsetTop = getCurve(i, curveMagnitude, playerState);
float curveOffsetBottom = getCurve(i + 1, curveMagnitude, playerState);
// FRONT FACE (outside)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, y4, z2 + curveOffsetBottom),
new Vec3d(x1, y4, z2 + curveOffsetBottom),
new Vec3d(x1, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z2 + curveOffsetTop),
frontU1, frontV2 - (frontPartV * i),
frontU2, frontV2 - (frontPartV * (i + 1)),
light, overlay,
0.0f, 0.0f, 1.0f, false);
// BACK FACE (inside)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x1, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z1 + curveOffsetTop),
backU1, backV2 - (backPartV * i),
backU2, backV2 - (backPartV * (i + 1)),
light, overlay,
0.0f, 0.0f, 1.0f, false);
// LEFT FACE (thickness at x2)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y4, z2 + curveOffsetBottom),
new Vec3d(x2, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z1 + curveOffsetTop),
leftU1, leftV2 - (leftPartV * i),
leftU2, leftV2 - (leftPartV * (i + 1)),
light, overlay,
1.0f, 0.0f, 0.0f, false);
// RIGHT FACE (thickness at x1)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x1, y4, z2 + curveOffsetBottom),
new Vec3d(x1, y4, z1 + curveOffsetBottom),
new Vec3d(x1, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z2 + curveOffsetTop),
rightU1, rightV2 - (rightPartV * i),
rightU2, rightV2 - (rightPartV * (i + 1)),
light, overlay,
-1.0f, 0.0f, 0.0f, false);
}
// TOP FACE (shoulders edge) – use its dedicated strip
matrixStack.push();
matrixStack.translate(0.0f, y2 + capeDepth, 0.0f);
matrixStack.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(90.0f));
matrixStack.translate(0.0f, -y2, 0.0f);
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, y2 + capeDepth, z2), new Vec3d(x1, y2 + capeDepth, z2),
new Vec3d(x1, y2, z2), new Vec3d(x2, y2, z2),
topU1, topV1, topU2, topV2,
light, overlay,
0.0f, 1.0f, 0.0f, true);
matrixStack.pop();
// BOTTOM FACE – use bottom strip
float curveOffset = getCurve(0, curveMagnitude, playerState);
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, 0.0f, z2 + curveOffset), new Vec3d(x1, 0.0f, z2 + curveOffset),
new Vec3d(x1, 0.0f, z1 + curveOffset), new Vec3d(x2, 0.0f, z1 + curveOffset),
bottomU1, bottomV1, bottomU2, bottomV2,
light, overlay,
0.0f, 0.0f, 1.0f, false);
matrixStack.pop();
}
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible) {
return;
}
if (playerEntityRenderState.skinTextures.capeTexture() != null) {
return;
}
String name = playerEntityRenderState.name;
String uuid = Auth.playerNames.get(name);
if (uuid == null || !Auth.players.containsKey(uuid)) {
return;
}
Identifier customCape = Cloaks.getCurrentCloakTexture(uuid);
if (customCape == null || this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack,
LayerType.WINGS)) {
return;
}
int minBrightness = 7;
int blockLight = (light >> 4) & 0xF;
int skyLight = (light >> 20) & 0xF;
blockLight = Math.max(blockLight, minBrightness);
skyLight = Math.max(skyLight, minBrightness);
light = (skyLight << 20) | (blockLight << 4);
matrixStack.push();
VertexConsumer vertexConsumer = vertexConsumerProvider
.getBuffer(RenderLayer.getEntityAlpha(customCape));
float targetVelocity = ((6.0F + playerEntityRenderState.field_53537 / 2.0F
+ playerEntityRenderState.field_53536) * 0.02f);
targetVelocity = Math.max(0.0f, targetVelocity - 0.1f);
float accelerationRate = 0.02f;
this.currentVelocity = this.currentVelocity
+ (targetVelocity - this.currentVelocity) * accelerationRate;
float rotation = this.currentVelocity * 72.0f;
float curve = this.currentVelocity * 0.4f;
if (playerEntityRenderState.sneaking && playerEntityRenderState.isInSneakingPose) {
matrixStack.translate(0, 0.24f, 0);
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(27.0f));
curve += 0.2f;
}
matrixStack.translate(0.0f, -0.25f, 0.0f);
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack,
LayerType.HUMANOID)) {
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
}
if (playerEntityRenderState.isSwimming) {
curve = 0.0f;
rotation = 0.0f;
}
if (!SaturnClientConfig.bendyCloaks.value) {
curve = 0.0f;
}
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light,
OverlayTexture.DEFAULT_UV, Math.min(rotation, 60.0f), Math.min(curve, 0.5f));
matrixStack.pop();
}
}
@@ -1,17 +0,0 @@
package org.saturnclient.saturnclient.cosmetics;
import java.util.ArrayList;
import java.util.List;
public class Emotes {
public static List<String> availableEmotes = new ArrayList<>();
public static void initialize() {
availableEmotes.add("tpose");
availableEmotes.add("back_flip");
availableEmotes.add("facepalm");
availableEmotes.add("cry");
availableEmotes.add("giga_chad");
availableEmotes.add("front_flip");
}
}
@@ -1,66 +0,0 @@
package org.saturnclient.saturnclient.cosmetics;
import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.SaturnClient;
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.render.item.ItemRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.EquippableComponent;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.item.ModelTransformationMode;
import net.minecraft.util.Identifier;
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) {
ItemStack headItem = state.equippedHeadStack;
String uuid = Auth.playerNames.get(state.name);
if (headItem.isEmpty() && uuid != null && !state.invisible) {
SaturnPlayer player = Auth.players.get(uuid);
if (player == null || player.hat == null || player.hat.isEmpty()) {
return;
}
matrices.push();
this.getContextModel().head.rotate(matrices);
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
ItemStack customHat = new ItemStack(Items.STICK);
customHat.set(DataComponentTypes.ITEM_MODEL,
Identifier.of("saturnclient:hat_" + player.hat));
customHat.set(DataComponentTypes.EQUIPPABLE,
EquippableComponent.builder(EquipmentSlot.HEAD).build());
ItemRenderer itemRenderer = SaturnClient.client.getItemRenderer();
itemRenderer.renderItem(
customHat,
ModelTransformationMode.THIRD_PERSON_RIGHT_HAND,
light,
OverlayTexture.DEFAULT_UV,
matrices,
vertexConsumers,
null,
0);
matrices.pop();
}
}
}
@@ -1,29 +0,0 @@
package org.saturnclient.saturnclient.cosmetics;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.auth.Auth;
public class Hats {
public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo", "halo_black" };
public static List<String> availableHats = new ArrayList<>();
public static void initialize() {
availableHats.add(0, "");
}
public static void setHat(String uuid, String hatName) {
setHatSilent(uuid, hatName);
Auth.setHat(hatName);
}
public static void setHatSilent(String uuid, String hatName) {
SaturnPlayer player = Auth.players.get(uuid);
if (player == null) {
Auth.players.put(uuid, new SaturnPlayer("", hatName));
} else {
player.hat = hatName;
}
}
}

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