Compare commits

...
68 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
190 changed files with 2914 additions and 8716 deletions
+8 -3
View File
@@ -19,15 +19,20 @@ jobs:
# The JDK comes from the runner's tool cache. `cache: gradle` stores Gradle's # 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 # dependencies, Loom's Minecraft cache and the build cache in the runner's
# actions cache. # actions cache.
- name: Set up Java 21 # 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 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: |
25
21
cache: gradle cache: gradle
- name: Compile all versions - name: Compile all versions
run: | run: |
chmod +x gradlew chmod +x gradlew
# --continue reports every version that fails, not just the first. # --continue reports every version that fails, not just the first.
./gradlew compileJava --no-daemon --stacktrace --continue ./gradlew compileJava --no-daemon --stacktrace --continue \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_21_X64,JAVA_HOME_25_X64
+10 -4
View File
@@ -47,18 +47,24 @@ jobs:
# The JDK comes from the runner's tool cache. `cache: gradle` stores Gradle's # 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 # dependencies, Loom's Minecraft cache and the build cache in the runner's
# actions cache. # actions cache.
- name: Set up Java 21 # 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 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: |
25
21
cache: gradle cache: gradle
- name: Build all versions - name: Build all versions
run: | run: |
chmod +x gradlew chmod +x gradlew
# buildAll remaps every versions/* project in parallel into build/allJars. # buildAll remaps every Minecraft version (two at a time, to stay within the
./gradlew buildAll --no-daemon --stacktrace # 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 - name: Upload jars to the release
env: env:
+4 -4
View File
@@ -18,7 +18,7 @@
Saturn Client is a free, open-source Minecraft client designed to enhance the gameplay experience while staying lightweight and customizable. 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. Our goal is to build a clean, optimized client that the community can contribute to and improve together.
Saturn runs on Fabric and supports **Minecraft 1.21.4 to 1.21.11**, with one build per version. If something doesn't work on your version, please open an issue. 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 ## Features
@@ -47,11 +47,11 @@ git clone --recurse-submodules https://git.selimaj.dev/saturnclientmc/saturnclie
cd saturnclient cd saturnclient
./gradlew buildAll # every version → build/allJars/ ./gradlew buildAll # every version → build/allJars/
./gradlew :1.21.11:build # a single version (versions not on Stonecutter yet: :mc-1.21.5) ./gradlew :1.21.11:build # a single version
./gradlew :1.21.11:runClient # launch a development client ./gradlew :1.21.11:runClient # launch a development client
``` ```
Building needs Java 21. Building needs Java 21. The 26.x versions compile with Java 25, which Gradle downloads when it isn't installed.
## Contributing ## Contributing
@@ -65,7 +65,7 @@ We welcome contributions of all kinds:
If you're an artist or designer interested in helping with cosmetics or visual assets, we'd love to collaborate. If you're an artist or designer interested in helping with cosmetics or visual assets, we'd love to collaborate.
Most features belong in `common`, which every Minecraft version shares. For code that differs between versions, see the [version guide](VERSION_GUIDE.md): it covers how the versions are laid out, porting a change to all of them with `scripts/port.sh`, and adding a new Minecraft version. Every pull request is compiled for all versions automatically. Most features belong in `common`, which every Minecraft version shares. For code that differs between versions, see the [version guide](VERSION_GUIDE.md): it covers how the versions share one copy of the code with Stonecutter, writing version-specific code, and adding a new Minecraft version. Every pull request is compiled for all versions automatically.
## Credits ## Credits
+34 -35
View File
@@ -4,13 +4,27 @@ How Saturn Client supports several Minecraft versions, and how to change or add
## Layout ## Layout
Saturn is moving from one full copy of the version-specific code per Minecraft version to a single copy built with [Stonecutter](https://stonecutter.kikugie.dev/) (issue #8). During the move there are two kinds of version: 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`. - `common/`: all version-independent code (mods, UI, cosmetics, the server client). It has no Minecraft dependency and reaches the game only through the interfaces in `org.saturnclient.common`.
- `src/`: the version-specific code (providers, refs, mixins and the three mixin configs) for the versions built by Stonecutter, currently **1.21.6 to 1.21.11**. It's one copy: where Minecraft's API differs between them, the code goes behind version comments (see below). Each of these versions is the Gradle project `:<mc>`, and `versions/<mc>/` holds only its `gradle.properties` (Minecraft, Yarn, Fabric Loader and Fabric API versions), `build/` and `run/`. - `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>/src/`: the versions not moved yet (1.21.4 and 1.21.5), each a full copy, as the Gradle project `:mc-<mc>` with its own `build.gradle`. - `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 Stonecutter versions in `stonecutterVersions` and includes every other folder under `versions/` as `:mc-<mc>`. The root `build.gradle.kts` builds each Stonecutter version, and `stonecutter.gradle.kts` configures the root project (shared repositories, the `common` wiring and `buildAll`). Loom's version for the Stonecutter versions is `loom_version` in the root `gradle.properties`. `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 ## Version-specific code with Stonecutter
@@ -18,29 +32,29 @@ The code in `src/` is always plain Java for the **active** version (1.21.11, set
```java ```java
//? if >=1.21.9 { //? if >=1.21.9 {
public void render(MatrixStack matrices, OrderedRenderCommandQueue queue, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) { public void submit(PoseStack matrices, SubmitNodeCollector output, int light, AvatarRenderState state, float limbAngle, float limbDistance) {
//?} else //?} else
//public void render(MatrixStack matrices, VertexConsumerProvider consumers, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) { //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 `/* … */`: A condition without braces covers the next line only. For several lines, use braces and wrap the inactive block in `/* … */`:
```java ```java
//? if >=1.21.11 { //? if >=1.21.11 {
import net.minecraft.client.render.RenderLayers; import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.render.RenderSetup; import net.minecraft.client.renderer.rendertype.RenderSetup;
//?} else { //?} else {
/*import net.minecraft.client.render.RenderPhase.Textures; /*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. Stonecutter rewrites inactive code into its own layout when switching versions (a single inactive line becomes `//code`), so write it that way to begin with, or run a switch and a reset before committing.
When a newer version leaves something out that an older one implements, keep the older version's code behind a condition and mark the newer branch with a `TODO`, rather than dropping it for every version. `WorldFeatureImpl.getWorldAge` and `SaturnScreenFabric.resize` are examples. Search for `TODO` to find what newer versions still lack. 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 `NametagsMixin`, which hooks a method that 1.21.9 changed) can keep an empty class body on the other versions. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments. A whole class that only exists for some versions (such as `MatrixStackMixin`, which only makes sense before 1.21.6's GUI rewrite) can keep an empty class body on the other versions. Before splitting a class like that, look for a hook that works on every version: nametags used to hook label rendering, which 1.21.9 changed, and now replace the name while the render state is built (`LivingEntityRendererMixin`), with no version condition at all. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments.
Inactive code sits inside a `/* … */` comment, so it can't contain `/* … */` comments itself: use `//` comments in code that's only active for some versions. For the same reason, don't nest multi-line version blocks; use `//?} else if <condition> {` for more than two version ranges, as in `PlayerEntityRendererMixin` and `SaturnRenderState`. 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 "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. - `./gradlew "Reset active project"` switches back to 1.21.11. **Run it before committing**, so `src/` is always committed with 1.21.11 active.
@@ -48,36 +62,21 @@ Inactive code sits inside a `/* … */` comment, so it can't contain `/* … */`
The `emotes` repository uses the same setup, and its `BentCubeRenderer` and `PlayerModelMixin` are small examples of version comments. The `emotes` repository uses the same setup, and its `BentCubeRenderer` and `PlayerModelMixin` are small examples of version comments.
## Porting a change to the other versions ## Checking a change on every version
Make the change in one version, usually the newest, then run: 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
scripts/port.sh # port uncommitted changes to every other version
scripts/port.sh --dry-run # see what would apply cleanly first
scripts/port.sh --to 1.21.4,1.21.5 # only some versions
scripts/port.sh --commit <rev> # port what a commit changed instead
```
The script applies the diff under one version's source to each other version with `git apply --3way`. The Stonecutter `src/` counts as one version (1.21.11), so changes port between it and the old copies in both directions:
- **applied**: the files matched, and nothing else is needed.
- **CONFLICTS**: that version's code really differs. The file gets normal conflict markers, with the version's own code in `ours` and the ported change in `theirs`. Adapt the change to that version's API and resolve.
- **already has this change**: nothing to do, which is safe to see when re-running.
Only source is ported. `build.gradle` and `gradle.properties` are version-specific on purpose. Then build everything:
```sh ```sh
./gradlew compileJava ./gradlew compileJava
``` ```
The **Check** workflow runs the same compile on every pull request, so a version that was missed or mis-ported fails before merging. 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 ## Adding a Minecraft version
Add new versions to Stonecutter rather than copying a folder: 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`.
1. Create `versions/<new>/gradle.properties` with `minecraft_version`, `yarn_mappings`, `loader_version` and `fabric_api_version` (copy 1.21.11's and change the values; they're listed at <https://fabricmc.net/develop>). Loom's version is shared, in the root `gradle.properties`. 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`.
2. Add `<new>` to `stonecutterVersions` in `settings.gradle.kts`. When moving an existing `mc-<v>` version over instead, delete its `versions/<v>/src` and run `./gradlew :<v>:clean` once: the new project shares the old one's `build/` folder, and classes compiled from the removed sources would otherwise end up in the jar.
3. Build it with `./gradlew :<new>:compileJava` and put what Minecraft changed behind version comments. Check the mixin configs in `src/main/resources/*.mixins.json` as well: a mixin whose target changed fails at startup, not at compile time, so launch it with `./gradlew :<new>:runClient`.
4. The release workflow builds everything through `buildAll`, and "Publish to Modrinth" takes the game version from each jar's name, so neither needs changing. 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.
+10 -7
View File
@@ -1,11 +1,14 @@
// Builds one Minecraft version from the shared src/. Stonecutter runs this for each version in // 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. // stonecutterVersions (settings.gradle.kts), with that version's versions/<v>/gradle.properties.
plugins { plugins {
id("net.fabricmc.fabric-loom-remap") id("dev.kikugie.loom-back-compat")
`maven-publish` `maven-publish`
} }
val minecraftVersion = property("minecraft_version") as String 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 val loaderVersion = property("loader_version") as String
version = "${property("mod_version")}+$minecraftVersion" version = "${property("mod_version")}+$minecraftVersion"
@@ -17,7 +20,7 @@ base {
dependencies { dependencies {
minecraft("com.mojang:minecraft:$minecraftVersion") minecraft("com.mojang:minecraft:$minecraftVersion")
mappings("net.fabricmc:yarn:${property("yarn_mappings")}:v2") loomx.applyMojangMappings()
modImplementation("net.fabricmc:fabric-loader:$loaderVersion") modImplementation("net.fabricmc:fabric-loader:$loaderVersion")
modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_api_version")}") modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_api_version")}")
@@ -42,23 +45,23 @@ dependencies {
tasks.processResources { tasks.processResources {
val values = mapOf( val values = mapOf(
"version" to project.version.toString(), "version" to project.version.toString(),
"minecraft_version" to minecraftVersion, "minecraft_version" to minecraftCompat,
"loader_version" to loaderVersion, "loader_version" to loaderVersion,
"java" to "JAVA_$javaVersion",
) )
inputs.properties(values) inputs.properties(values)
filteringCharset = "UTF-8" filteringCharset = "UTF-8"
filesMatching("fabric.mod.json") { expand(values) } filesMatching(listOf("fabric.mod.json", "*.mixins.json")) { expand(values) }
} }
tasks.withType<JavaCompile>().configureEach { tasks.withType<JavaCompile>().configureEach {
options.release = 21 options.release = javaVersion
} }
java { java {
// Loom attaches the sources jar to remapSourcesJar and build. // Loom attaches the sources jar to remapSourcesJar and build.
withSourcesJar() withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21 toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
targetCompatibility = JavaVersion.VERSION_21
} }
tasks.jar { tasks.jar {
+1 -1
Submodule common updated: 15fa973553...e8f686b2d3
+4 -4
View File
@@ -6,10 +6,10 @@ org.gradle.caching=true
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 # IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
org.gradle.configuration-cache=false org.gradle.configuration-cache=false
mod_version=0.1.1-beta mod_version=0.1.5-beta
# Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes) # Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes)
emotes_version=0.1.0 emotes_version=0.1.2
# Loom, for the Stonecutter versions (settings.gradle.kts). Old mc-<v> projects set their own. # Loom, for every Minecraft version (read by loom-back-compat).
loom_version=1.17.21 loomx.loom_version=1.17.21
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 591 KiB

-184
View File
@@ -1,184 +0,0 @@
#!/usr/bin/env bash
# Port a change made in one Minecraft version's code to the other versions.
#
# Takes the diff under one version's source folder and applies it to the others
# with `git apply --3way`: it applies cleanly where the files match, and leaves
# normal merge conflicts only where a version really differs. Only source is
# ported; build.gradle and gradle.properties are version-specific on purpose.
#
# Versions moved to Stonecutter (stonecutterVersions in settings.gradle.kts)
# share the top-level src/, which counts as one version: the active one set in
# stonecutter.gradle.kts. The others keep their own copy in versions/<v>/src.
#
# Usage:
# scripts/port.sh [--from VERSION] [--to V1,V2,...] [--commit REV] [--dry-run]
#
# --from version the change was made in. Default: the only version with
# uncommitted changes (or changed by --commit).
# --to comma-separated versions to port to. Default: all others.
# --commit port the changes a commit made instead of uncommitted changes.
# --dry-run report what would happen without changing anything.
#
# Exit status: 0 if every version applied cleanly (or already had the change),
# 1 if any version has conflicts or failed.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
from=""
to=""
commit=""
dry_run=false
while [ $# -gt 0 ]; do
case "$1" in
--from) from="$2"; shift 2 ;;
--to) to="$2"; shift 2 ;;
--commit) commit="$2"; shift 2 ;;
--dry-run) dry_run=true; shift ;;
-h|--help) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Unknown option: $1 (see --help)" >&2; exit 2 ;;
esac
done
# Versions sharing the Stonecutter src/, and the one that stands for them.
stonecutter_versions=$(sed -n 's/^val stonecutterVersions = listOf(\(.*\))/\1/p' settings.gradle.kts | tr -d '" ' | tr ',' ' ')
stonecutter_active=$(sed -n 's/^stonecutter active "\(.*\)"/\1/p' stonecutter.gradle.kts 2>/dev/null || true)
is_stonecutter() {
printf '%s\n' $stonecutter_versions | grep -qx "$1"
}
# Where a version's source lives.
src_of() {
if is_stonecutter "$1"; then echo "src"; else echo "versions/$1/src"; fi
}
old_versions=$(for d in versions/*/; do [ -d "$d/src" ] && basename "$d"; done)
all_versions=$(printf '%s\n' $old_versions $stonecutter_active | grep . | sort -t. -k1,1n -k2,2n -k3,3n)
version_exists() {
[ -d "$(src_of "$1")" ]
}
# Versions with changes, from the working tree or from the given commit.
changed_versions() {
if [ -n "$commit" ]; then
git show --name-only --format= "$commit" -- versions/ src/
else
git diff HEAD --name-only -- versions/ src/
git ls-files --others --exclude-standard -- versions/ src/
fi | sed -n -e 's#^versions/\([^/]*\)/src/.*#\1#p' -e "s#^src/.*#${stonecutter_active}#p" | sort -u
}
if [ -z "$from" ]; then
candidates=$(changed_versions)
count=$(printf '%s\n' "$candidates" | grep -c . || true)
if [ "$count" -ne 1 ]; then
echo "Changes found in ${count} versions (${candidates//$'\n'/, }); pass --from to choose one." >&2
exit 2
fi
from="$candidates"
fi
version_exists "$from" || { echo "No such version: $from" >&2; exit 2; }
if is_stonecutter "$from"; then
from="$stonecutter_active"
fi
from_src=$(src_of "$from")
if [ -z "$to" ]; then
to=$(printf '%s\n' $all_versions | grep -vx "$from" | paste -sd, -)
fi
# New files are only included in `git diff HEAD` once git knows about them.
if [ -z "$commit" ]; then
untracked=$(git ls-files --others --exclude-standard -- "$from_src")
if [ -n "$untracked" ]; then
if $dry_run; then
echo "note: new files in $from_src are untracked and won't be ported in a dry run:" >&2
printf ' %s\n' $untracked >&2
else
printf '%s\n' "$untracked" | xargs git add --intent-to-add --
fi
fi
fi
# --full-index gives --3way the real blob ids to merge against.
if [ -n "$commit" ]; then
patch=$(git show --full-index --binary --format= "$commit" -- "$from_src")
else
patch=$(git diff HEAD --full-index --binary -- "$from_src")
fi
if [ -z "$patch" ]; then
echo "No changes under $from_src to port." >&2
exit 2
fi
files=$(printf '%s\n' "$patch" | grep -c '^diff --git' || true)
echo "Porting $files file(s) from $from${commit:+ (commit $commit)}"
from_re=$(printf '%s' "$from_src" | sed 's/\./\\./g')
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
status=0
IFS=',' read -r -a targets <<< "$to"
for target in "${targets[@]}"; do
if [ "$target" = "$from" ]; then
continue
fi
if is_stonecutter "$target" && [ "$target" != "$stonecutter_active" ]; then
printf ' %-8s skipped: shares src/ with %s\n' "$target" "$stonecutter_active"
[ "$from" = "$stonecutter_active" ] || status=1
continue
fi
if ! version_exists "$target"; then
printf ' %-8s skipped: no such version\n' "$target"
status=1
continue
fi
# Point the patch's file headers at the target version's source folder.
target_src=$(src_of "$target")
printf '%s\n' "$patch" | sed -E "/^(diff --git |--- |\+\+\+ |rename (from|to) |copy (from|to) )/ s#([ab]/|(from|to) )${from_re}/#\1${target_src}/#g" > "$tmp/$target.patch"
if git apply --check --reverse "$tmp/$target.patch" 2>/dev/null; then
printf ' %-8s already has this change\n' "$target"
continue
fi
if $dry_run; then
if git apply --check "$tmp/$target.patch" 2>/dev/null; then
printf ' %-8s would apply cleanly\n' "$target"
else
printf ' %-8s would need a 3-way merge (possible conflicts)\n' "$target"
fi
continue
fi
if output=$(git apply --3way "$tmp/$target.patch" 2>&1); then
printf ' %-8s applied\n' "$target"
else
conflicts=$(git diff --name-only --diff-filter=U -- "$target_src")
if [ -n "$conflicts" ]; then
printf ' %-8s CONFLICTS in:\n' "$target"
printf '%s\n' "$conflicts" | sed 's/^/ /'
else
printf ' %-8s FAILED:\n' "$target"
printf '%s\n' "$output" | sed 's/^/ /'
fi
status=1
fi
done
if ! $dry_run; then
echo
echo "Ported changes are staged in the target versions. Resolve any conflicts, then build"
echo "the affected versions, e.g. ./gradlew compileJava"
fi
exit $status
+10 -18
View File
@@ -5,17 +5,15 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
mavenCentral() mavenCentral()
} }
// Loom's version, for the Stonecutter versions' build.gradle.kts (the old mc-<v> projects set
// their own in versions/<v>/gradle.properties).
val loom_version: String by settings
plugins {
id("net.fabricmc.fabric-loom-remap") version loom_version
}
} }
plugins { 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" 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" rootProject.name = "saturn-client"
@@ -23,8 +21,11 @@ rootProject.name = "saturn-client"
include("common") include("common")
// Versions built by Stonecutter from the shared src/ (see stonecutter.gradle.kts). Each one's // Versions built by Stonecutter from the shared src/ (see stonecutter.gradle.kts). Each one's
// versions/<v>/gradle.properties holds its Minecraft, Yarn and Fabric API versions. // versions/<v>/gradle.properties holds its Minecraft, Fabric Loader and Fabric API versions.
val stonecutterVersions = listOf("1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11") 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 { stonecutter {
create(rootProject) { create(rootProject) {
@@ -32,12 +33,3 @@ stonecutter {
vcsVersion = "1.21.11" vcsVersion = "1.21.11"
} }
} }
// Versions not moved to Stonecutter yet still build from their own copy in versions/<v>/src,
// as projects named mc-<v>.
file("versions").listFiles { file -> file.isDirectory && file.name !in stonecutterVersions }!!
.sortedBy { it.name }
.forEach { dir ->
include("mc-${dir.name}")
project(":mc-${dir.name}").projectDir = dir
}
@@ -1,40 +1,51 @@
package org.saturnclient.impl.cosmetics; package org.saturnclient.impl.cosmetics;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import org.joml.Matrix3f; import org.joml.Matrix3f;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector4f; import org.joml.Vector4f;
import org.saturnclient.client.player.SaturnPlayer; import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Config; import org.saturnclient.config.Config;
import org.saturnclient.cosmetics.Cloaks; 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.impl.cosmetics.utils.ShaderUtils;
import org.saturnclient.saturnclient.SaturnRenderState; import org.saturnclient.saturnclient.SaturnRenderState;
import net.minecraft.client.render.OverlayTexture; import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.render.VertexConsumer; import net.minecraft.client.renderer.texture.OverlayTexture;
import com.mojang.blaze3d.vertex.VertexConsumer;
//? if >=1.21.9 { //? if >=1.21.9 {
import net.minecraft.client.render.command.OrderedRenderCommandQueue; import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else //?} else
//import net.minecraft.client.render.VertexConsumerProvider; //import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.render.entity.equipment.EquipmentModel; import net.minecraft.client.resources.model.EquipmentClientInfo;
import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; import net.minecraft.client.resources.model.EquipmentAssetManager;
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType; import net.minecraft.client.resources.model.EquipmentClientInfo.LayerType;
import net.minecraft.client.render.entity.feature.FeatureRenderer; import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext; import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.render.entity.model.PlayerEntityModel; //? if >=1.21.11 {
import net.minecraft.client.render.entity.state.PlayerEntityRenderState; import net.minecraft.client.model.player.PlayerModel;
import net.minecraft.client.util.math.MatrixStack; //?} else
import net.minecraft.component.DataComponentTypes; //import net.minecraft.client.model.PlayerModel;
import net.minecraft.component.type.EquippableComponent; import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import net.minecraft.item.ItemStack; import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.item.equipment.EquipmentAsset; import net.minecraft.core.component.DataComponents;
import net.minecraft.registry.RegistryKey; import net.minecraft.world.item.equipment.Equippable;
import net.minecraft.util.math.MathHelper; import net.minecraft.world.item.ItemStack;
import net.minecraft.util.math.RotationAxis; import net.minecraft.world.item.equipment.EquipmentAsset;
import net.minecraft.util.math.Vec3d; import net.minecraft.resources.ResourceKey;
import net.minecraft.util.Mth;
/** /**
* The cloak renderer * The cloak renderer
@@ -43,8 +54,8 @@ import net.minecraft.util.math.Vec3d;
* - **Curving**: Curves when player velocity increases. * - **Curving**: Curves when player velocity increases.
* - **Wavey**: Makes the cloak feel like wind, when jump sprinting. * - **Wavey**: Makes the cloak feel like wind, when jump sprinting.
*/ */
public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> { public class CloakFeatureRenderer extends RenderLayer<AvatarRenderState, PlayerModel> {
private final EquipmentModelLoader equipmentModelLoader; private final EquipmentAssetManager equipmentModelLoader;
private static final int PARTS = 24; private static final int PARTS = 24;
private final float[] segmentValues = new float[PARTS]; private final float[] segmentValues = new float[PARTS];
private static final int H_PARTS = 6; private static final int H_PARTS = 6;
@@ -87,19 +98,19 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8); private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8); private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context, public CloakFeatureRenderer(RenderLayerParent<AvatarRenderState, PlayerModel> context,
EquipmentModelLoader equipmentModelLoader) { EquipmentAssetManager equipmentModelLoader) {
super(context); super(context);
this.equipmentModelLoader = equipmentModelLoader; this.equipmentModelLoader = equipmentModelLoader;
Arrays.fill(segmentValues, 0.0f); Arrays.fill(segmentValues, 0.0f);
} }
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) { private boolean hasCustomModelForLayer(ItemStack stack, EquipmentClientInfo.LayerType layerType) {
EquippableComponent equippableComponent = (EquippableComponent) stack Equippable equippableComponent = (Equippable) stack
.get(DataComponentTypes.EQUIPPABLE); .get(DataComponents.EQUIPPABLE);
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) { if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
EquipmentModel equipmentModel = this.equipmentModelLoader EquipmentClientInfo equipmentModel = this.equipmentModelLoader
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get()); .get((ResourceKey<EquipmentAsset>) equippableComponent.assetId().get());
return !equipmentModel.getLayers(layerType).isEmpty(); return !equipmentModel.getLayers(layerType).isEmpty();
} else { } else {
return false; return false;
@@ -107,7 +118,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
} }
private void vertex(VertexConsumer vc, Matrix4f posMat, Matrix3f normalMat, private void vertex(VertexConsumer vc, Matrix4f posMat, Matrix3f normalMat,
Vec3d pos, int color, net.minecraft.world.phys.Vec3 pos, int color,
float u, float v, int overlay, int light, float u, float v, int overlay, int light,
float nx, float ny, float nz) { float nx, float ny, float nz) {
@@ -117,7 +128,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
Vector3f n = new Vector3f(nx, ny, nz); Vector3f n = new Vector3f(nx, ny, nz);
n.mul(normalMat); n.mul(normalMat);
vc.vertex( vc.addVertex(
p.x(), p.y(), p.z(), p.x(), p.y(), p.z(),
color, color,
u, v, u, v,
@@ -125,13 +136,13 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
n.x(), n.y(), n.z()); n.x(), n.y(), n.z());
} }
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack.Entry entry, private void renderCapeQuad(VertexConsumer vertexConsumer, PoseStack.Pose entry,
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft, 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 u1, float v1, float u2, float v2, int light, int overlay,
float normalX, float normalY, float normalZ, boolean flipWinding) { float normalX, float normalY, float normalZ, boolean flipWinding) {
// Extract matrix (you must transform positions manually now) // Extract matrix (you must transform positions manually now)
Matrix4f pos = entry.getPositionMatrix(); Matrix4f pos = entry.pose();
Matrix3f normalMat = entry.getNormalMatrix(); Matrix3f normalMat = entry.normal();
int color = 0xFFFFFFFF; // white RGBA int color = 0xFFFFFFFF; // white RGBA
@@ -154,8 +165,8 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
} }
} }
private void renderCape(MatrixStack.Entry entry, VertexConsumer vertexConsumer, private void renderCape(PoseStack.Pose entry, VertexConsumer vertexConsumer,
PlayerEntityRenderState playerState, int light, int overlay) { AvatarRenderState playerState, int light, int overlay) {
float capeWidth = 10.0f / 16.0f; float capeWidth = 10.0f / 16.0f;
float capeHeight = 16.0f / 16.0f; float capeHeight = 16.0f / 16.0f;
@@ -217,8 +228,8 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
// 2. Grid // 2. Grid
// ============================= // =============================
Vec3d[][] inner = new Vec3d[H_PARTS + 1][PARTS + 1]; net.minecraft.world.phys.Vec3[][] inner = new net.minecraft.world.phys.Vec3[H_PARTS + 1][PARTS + 1];
Vec3d[][] outer = new Vec3d[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++) { for (int x = 0; x <= H_PARTS; x++) {
@@ -250,8 +261,8 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
float thickY = sinA * capeDepth; float thickY = sinA * capeDepth;
float thickZ = cosA * capeDepth; float thickZ = cosA * capeDepth;
inner[x][i] = new Vec3d(xPos, y - curveY, z - curveZ); inner[x][i] = new net.minecraft.world.phys.Vec3(xPos, y - curveY, z - curveZ);
outer[x][i] = new Vec3d(xPos, (y - curveY) - thickY, (z - curveZ) - thickZ); outer[x][i] = new net.minecraft.world.phys.Vec3(xPos, (y - curveY) - thickY, (z - curveZ) - thickZ);
} }
} }
@@ -276,15 +287,15 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
float bvStart = backV1 + backPartV * i; float bvStart = backV1 + backPartV * i;
float bvEnd = backV1 + backPartV * (i + 1); float bvEnd = backV1 + backPartV * (i + 1);
Vec3d innerTL = inner[x + 1][i]; net.minecraft.world.phys.Vec3 innerTL = inner[x + 1][i];
Vec3d innerTR = inner[x][i]; net.minecraft.world.phys.Vec3 innerTR = inner[x][i];
Vec3d innerBL = inner[x + 1][i + 1]; net.minecraft.world.phys.Vec3 innerBL = inner[x + 1][i + 1];
Vec3d innerBR = inner[x][i + 1]; net.minecraft.world.phys.Vec3 innerBR = inner[x][i + 1];
Vec3d outerTL = outer[x + 1][i]; net.minecraft.world.phys.Vec3 outerTL = outer[x + 1][i];
Vec3d outerTR = outer[x][i]; net.minecraft.world.phys.Vec3 outerTR = outer[x][i];
Vec3d outerBL = outer[x + 1][i + 1]; net.minecraft.world.phys.Vec3 outerBL = outer[x + 1][i + 1];
Vec3d outerBR = outer[x][i + 1]; net.minecraft.world.phys.Vec3 outerBR = outer[x][i + 1];
// FRONT // FRONT
renderCapeQuad(vertexConsumer, entry, renderCapeQuad(vertexConsumer, entry,
@@ -306,10 +317,10 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
for (int i = 0; i < PARTS; i++) { for (int i = 0; i < PARTS; i++) {
Vec3d innerTop = inner[H_PARTS][i]; net.minecraft.world.phys.Vec3 innerTop = inner[H_PARTS][i];
Vec3d innerBot = inner[H_PARTS][i + 1]; net.minecraft.world.phys.Vec3 innerBot = inner[H_PARTS][i + 1];
Vec3d outerTop = outer[H_PARTS][i]; net.minecraft.world.phys.Vec3 outerTop = outer[H_PARTS][i];
Vec3d outerBot = outer[H_PARTS][i + 1]; net.minecraft.world.phys.Vec3 outerBot = outer[H_PARTS][i + 1];
float vStart = leftV1 + leftPartV * i; float vStart = leftV1 + leftPartV * i;
float vEnd = leftV1 + leftPartV * (i + 1); float vEnd = leftV1 + leftPartV * (i + 1);
@@ -327,10 +338,10 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
for (int i = 0; i < PARTS; i++) { for (int i = 0; i < PARTS; i++) {
Vec3d innerTop = inner[0][i]; net.minecraft.world.phys.Vec3 innerTop = inner[0][i];
Vec3d innerBot = inner[0][i + 1]; net.minecraft.world.phys.Vec3 innerBot = inner[0][i + 1];
Vec3d outerTop = outer[0][i]; net.minecraft.world.phys.Vec3 outerTop = outer[0][i];
Vec3d outerBot = outer[0][i + 1]; net.minecraft.world.phys.Vec3 outerBot = outer[0][i + 1];
float vStart = rightV1 + rightPartV * i; float vStart = rightV1 + rightPartV * i;
float vEnd = rightV1 + rightPartV * (i + 1); float vEnd = rightV1 + rightPartV * (i + 1);
@@ -348,10 +359,10 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
for (int x = 0; x < H_PARTS; x++) { for (int x = 0; x < H_PARTS; x++) {
Vec3d outerL = outer[x][0]; net.minecraft.world.phys.Vec3 outerL = outer[x][0];
Vec3d outerR = outer[x + 1][0]; net.minecraft.world.phys.Vec3 outerR = outer[x + 1][0];
Vec3d innerR = inner[x + 1][0]; net.minecraft.world.phys.Vec3 innerR = inner[x + 1][0];
Vec3d innerL = inner[x][0]; net.minecraft.world.phys.Vec3 innerL = inner[x][0];
float uStart = topU1 + (topU2 - topU1) * ((float) x / H_PARTS); float uStart = topU1 + (topU2 - topU1) * ((float) x / H_PARTS);
float uEnd = topU1 + (topU2 - topU1) * ((float) (x + 1) / H_PARTS); float uEnd = topU1 + (topU2 - topU1) * ((float) (x + 1) / H_PARTS);
@@ -368,10 +379,10 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
for (int x = 0; x < H_PARTS; x++) { for (int x = 0; x < H_PARTS; x++) {
Vec3d innerL = inner[x][PARTS]; net.minecraft.world.phys.Vec3 innerL = inner[x][PARTS];
Vec3d innerR = inner[x + 1][PARTS]; net.minecraft.world.phys.Vec3 innerR = inner[x + 1][PARTS];
Vec3d outerR = outer[x + 1][PARTS]; net.minecraft.world.phys.Vec3 outerR = outer[x + 1][PARTS];
Vec3d outerL = outer[x][PARTS]; net.minecraft.world.phys.Vec3 outerL = outer[x][PARTS];
float uStart = bottomU1 + (bottomU2 - bottomU1) * ((float) x / H_PARTS); float uStart = bottomU1 + (bottomU2 - bottomU1) * ((float) x / H_PARTS);
float uEnd = bottomU1 + (bottomU2 - bottomU1) * ((float) (x + 1) / H_PARTS); float uEnd = bottomU1 + (bottomU2 - bottomU1) * ((float) (x + 1) / H_PARTS);
@@ -383,6 +394,54 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
} }
} }
/** 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 // Shift array left and insert new value at the start
private void updateSegmentValues(float value) { private void updateSegmentValues(float value) {
// Shift all elements one position to the right (from end to start) // Shift all elements one position to the right (from end to start)
@@ -397,17 +456,20 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
private long lastUpdate = 0; private long lastUpdate = 0;
@Override @Override
public void render(MatrixStack matrices,
//? if >=1.21.9 { //? if >=1.21.9 {
OrderedRenderCommandQueue output, public void submit(PoseStack matrices,
//?} else //?} else
//VertexConsumerProvider output, //public void render(PoseStack matrices,
int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
if (state.invisible || !state.capeVisible
//? if >=1.21.9 { //? if >=1.21.9 {
|| state.skinTextures.cape() != null) { SubmitNodeCollector output,
//?} else //?} else
//|| state.skinTextures.capeTexture() != null) { //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; return;
} }
@@ -416,13 +478,13 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
//?} else //?} else
//SaturnPlayer player = SaturnPlayer.get(state.name); //SaturnPlayer player = SaturnPlayer.get(state.name);
if (player == null) { if (player == null || playingTorsoBend(player)) {
return; return;
} }
IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak); IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak);
if (customCape == null if (customCape == null
|| this.hasCustomModelForLayer(state.equippedChestStack, LayerType.WINGS)) { || this.hasCustomModelForLayer(state.chestEquipment, LayerType.WINGS)) {
return; return;
} }
@@ -434,26 +496,33 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
skyLight = Math.max(skyLight, minBrightness); skyLight = Math.max(skyLight, minBrightness);
light = (skyLight << 20) | (blockLight << 4); light = (skyLight << 20) | (blockLight << 4);
matrices.push(); 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); matrices.translate(0.0f, 0.0f, 0.12f);
if (this.hasCustomModelForLayer(state.equippedChestStack, LayerType.HUMANOID)) { if (this.hasCustomModelForLayer(state.chestEquipment, LayerType.HUMANOID)) {
matrices.translate(0.0F, -0.053125F, 0.06875F); matrices.translate(0.0F, -0.053125F, 0.06875F);
} }
if (state.isInSneakingPose) {
matrices.translate(0, 0.16f, 0.0f);
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f));
}
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (now - lastUpdate >= 20) { if (now - lastUpdate >= 20) {
float velX = Math.min(0.8f, state.field_53537 / 108.0f); float velX = Math.min(0.8f, state.capeLean / 108.0f);
float rawVelY = state.field_53536; float rawVelY = state.capeFlap;
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16; float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
float value = Math.max(0.0f, Math.min(2.0f, state.isSwimming ? 0.0f : velX + velY)); float value = Math.max(0.0f, Math.min(2.0f, state.isVisuallySwimming ? 0.0f : velX + velY));
if (Config.cloakPhysics.value) { if (Config.cloakPhysics.value) {
updateSegmentValues(segmentValues[0] + (value - segmentValues[0]) * 0.3f); updateSegmentValues(segmentValues[0] + (value - segmentValues[0]) * 0.3f);
@@ -465,15 +534,15 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
final int l = light; final int l = light;
horizontalCurve = MathHelper.clamp(state.field_53537 / 150.0f, -1.0f, 1.0f); horizontalCurve = Mth.clamp(state.capeLean / 150.0f, -1.0f, 1.0f);
//? if >=1.21.9 { //? if >=1.21.9 {
output.submitCustom(matrices, ShaderUtils.getRenderLayer(customCape), (entry, vertexConsumer) -> { output.submitCustomGeometry(matrices, ShaderUtils.getRenderLayer(customCape), (entry, vertexConsumer) -> {
renderCape(entry, vertexConsumer, state, l, OverlayTexture.DEFAULT_UV); renderCape(entry, vertexConsumer, state, l, OverlayTexture.NO_OVERLAY);
}); });
//?} else //?} else
//renderCape(matrices.peek(), output.getBuffer(ShaderUtils.getRenderLayer(customCape)), state, l, OverlayTexture.DEFAULT_UV); //renderCape(matrices.last(), output.getBuffer(ShaderUtils.getRenderLayer(customCape)), state, l, OverlayTexture.NO_OVERLAY);
matrices.pop(); matrices.popPose();
} }
} }
@@ -1,53 +1,65 @@
package org.saturnclient.impl.cosmetics; package org.saturnclient.impl.cosmetics;
import org.joml.Quaternionf;
import org.saturnclient.client.player.SaturnPlayer; import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.impl.cosmetics.obj.MtlLoader; import org.saturnclient.impl.cosmetics.obj.MtlLoader;
import org.saturnclient.impl.cosmetics.obj.ObjModel; import org.saturnclient.impl.cosmetics.obj.ObjModel;
import org.saturnclient.saturnclient.SaturnRenderState; import org.saturnclient.saturnclient.SaturnRenderState;
import net.minecraft.client.render.OverlayTexture; import net.minecraft.client.renderer.texture.OverlayTexture;
//? if >=1.21.9 { //? if >=1.21.9 {
import net.minecraft.client.render.command.OrderedRenderCommandQueue; import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else //?} else
//import net.minecraft.client.render.VertexConsumerProvider; //import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.render.entity.feature.FeatureRenderer; import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext; import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.render.entity.model.PlayerEntityModel; //? if >=1.21.11 {
import net.minecraft.client.render.entity.state.PlayerEntityRenderState; import net.minecraft.client.model.player.PlayerModel;
import net.minecraft.client.util.math.MatrixStack; //?} else
import net.minecraft.client.model.ModelPart; //import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import com.mojang.blaze3d.vertex.PoseStack;
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> { public class HatFeatureRenderer extends RenderLayer<AvatarRenderState, PlayerModel> {
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) { public HatFeatureRenderer(RenderLayerParent<AvatarRenderState, PlayerModel> context) {
super(context); super(context);
} }
@Override @Override
public void render(MatrixStack matrices,
//? if >=1.21.9 { //? if >=1.21.9 {
OrderedRenderCommandQueue output, public void submit(PoseStack matrices,
//?} else //?} else
//VertexConsumerProvider output, //public void render(PoseStack matrices,
int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) { //? if >=1.21.9 {
SubmitNodeCollector output,
//?} else
//MultiBufferSource output,
int light, AvatarRenderState state, float limbAngle, float limbDistance) {
//? if >=1.21.8 { //? if >=1.21.8 {
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey); SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
//?} else //?} else
//SaturnPlayer player = SaturnPlayer.get(state.name); //SaturnPlayer player = SaturnPlayer.get(state.name);
if (state.invisible || player == null || player.hat.isEmpty()) { if (state.isInvisible || player == null || player.hat.isEmpty()) {
return; return;
} }
matrices.push(); matrices.pushPose();
ModelPart head = this.getContextModel().head; // On the head, as vanilla places head items: the whole player (which emotes like the backflip
matrices.multiply(new Quaternionf().rotationZYX(head.roll, head.yaw, head.pitch)); // 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), ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, output, light, matrices, output, light,
OverlayTexture.DEFAULT_UV); OverlayTexture.NO_OVERLAY);
matrices.pop(); matrices.popPose();
} }
} }
@@ -8,15 +8,15 @@ import java.util.Map;
import de.javagl.obj.*; import de.javagl.obj.*;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
public class MtlLoader { public class MtlLoader {
static Map<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>(); static Map<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>();
static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException { static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager() try (InputStream is = Minecraft.getInstance().getResourceManager()
.getResource(mtlId).get().getInputStream()) { .getResource(mtlId).get().open()) {
List<Mtl> mtlList = MtlReader.read(is); List<Mtl> mtlList = MtlReader.read(is);
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>(); Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
for (Mtl mtl : mtlList) { for (Mtl mtl : mtlList) {
@@ -51,6 +51,6 @@ public class MtlLoader {
if (mtlId.isEmpty()) if (mtlId.isEmpty())
mtlId = "white"; mtlId = "white";
return of(Identifier.of("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl")); return of(Identifier.fromNamespaceAndPath("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl"));
} }
} }
@@ -12,15 +12,16 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import de.javagl.obj.Mtl; import de.javagl.obj.Mtl;
import de.javagl.obj.Obj; import de.javagl.obj.Obj;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
//? if >=1.21.9 { //? if >=1.21.9 {
import net.minecraft.client.render.command.OrderedRenderCommandQueue; import net.minecraft.client.renderer.SubmitNodeCollector;
//?} else //?} else
//import net.minecraft.client.render.VertexConsumerProvider; //import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.util.math.MatrixStack; import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.resource.Resource; import org.joml.Quaternionf;
import net.minecraft.resource.ResourceManager; import net.minecraft.server.packs.resources.Resource;
import net.minecraft.util.Identifier; import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.resources.Identifier;
public class ObjModel { public class ObjModel {
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -46,7 +47,7 @@ public class ObjModel {
Config config; Config config;
public static ObjModel cosmetic(String cosmeticKind, String id) { public static ObjModel cosmetic(String cosmeticKind, String id) {
return of(Identifier.of("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model")); return of(Identifier.fromNamespaceAndPath("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model"));
} }
public static ObjModel of(Identifier obj) { public static ObjModel of(Identifier obj) {
@@ -59,8 +60,8 @@ public class ObjModel {
} }
ObjModel(Identifier identifier) { ObjModel(Identifier identifier) {
Identifier objId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".obj"); Identifier objId = Identifier.fromNamespaceAndPath(identifier.getNamespace(), identifier.getPath() + ".obj");
Identifier jsonId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".json"); Identifier jsonId = Identifier.fromNamespaceAndPath(identifier.getNamespace(), identifier.getPath() + ".json");
try { try {
this.obj = ObjRenderer.loadObj(objId); this.obj = ObjRenderer.loadObj(objId);
@@ -69,11 +70,11 @@ public class ObjModel {
} }
// Try loading JSON config if it exists // Try loading JSON config if it exists
ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager(); ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
Optional<Resource> resource = resourceManager.getResource(jsonId); Optional<Resource> resource = resourceManager.getResource(jsonId);
if (resource.isPresent()) { if (resource.isPresent()) {
try (InputStream stream = resource.get().getInputStream()) { try (InputStream stream = resource.get().open()) {
this.config = MAPPER.readValue(stream, Config.class); this.config = MAPPER.readValue(stream, Config.class);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@@ -81,29 +82,37 @@ public class ObjModel {
} }
} }
public void render(Map<String, Mtl> mtlMap, MatrixStack matrices, public void render(Map<String, Mtl> mtlMap, PoseStack matrices,
//? if >=1.21.9 { //? if >=1.21.9 {
OrderedRenderCommandQueue output, SubmitNodeCollector output,
//?} else //?} else
//VertexConsumerProvider output, //MultiBufferSource output,
int light, int overlay) { int light, int overlay) {
matrices.push(); matrices.pushPose();
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f)); rotate(matrices, com.mojang.math.Axis.XP.rotationDegrees(180.0f));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f)); rotate(matrices, com.mojang.math.Axis.YP.rotationDegrees(180.0f));
if (this.config != null) { if (this.config != null) {
matrices.translate(config.transform.x, config.transform.y, config.transform.z); matrices.translate(config.transform.x, config.transform.y, config.transform.z);
matrices.scale(config.scale.x, config.scale.y, config.scale.z); matrices.scale(config.scale.x, config.scale.y, config.scale.z);
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(config.rotation.x)); rotate(matrices, com.mojang.math.Axis.XP.rotationDegrees(config.rotation.x));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(config.rotation.y)); rotate(matrices, com.mojang.math.Axis.YP.rotationDegrees(config.rotation.y));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Z.rotationDegrees(config.rotation.z)); rotate(matrices, com.mojang.math.Axis.ZP.rotationDegrees(config.rotation.z));
} }
ObjRenderer.renderObj(this.obj, mtlMap, matrices, output, light, overlay); ObjRenderer.renderObj(this.obj, mtlMap, matrices, output, light, overlay);
matrices.pop(); 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);
} }
} }
@@ -9,32 +9,35 @@ import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import de.javagl.obj.*; import de.javagl.obj.*;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.render.RenderLayer; //? if >=1.21.11 {
import net.minecraft.client.render.VertexConsumer; import net.minecraft.client.renderer.rendertype.RenderType;
//? if >=1.21.9 {
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
//?} else //?} else
//import net.minecraft.client.render.VertexConsumerProvider; //import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.util.math.MatrixStack; import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.util.Identifier; //? 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 class ObjRenderer {
public static Obj loadObj(Identifier objId) throws IOException { public static Obj loadObj(Identifier objId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager() try (InputStream is = Minecraft.getInstance().getResourceManager()
.getResource(objId).get().getInputStream()) { .getResource(objId).get().open()) {
return ObjReader.read(is); return ObjReader.read(is);
} }
} }
public static void renderObj(Obj obj, public static void renderObj(Obj obj,
Map<String, Mtl> mtlMap, Map<String, Mtl> mtlMap,
MatrixStack matrices, PoseStack matrices,
//? if >=1.21.9 { //? if >=1.21.9 {
OrderedRenderCommandQueue output, SubmitNodeCollector output,
//?} else //?} else
//VertexConsumerProvider output, //MultiBufferSource output,
int light, int light,
int overlay) { int overlay) {
@@ -65,20 +68,20 @@ public class ObjRenderer {
} }
} }
RenderLayer layer = ShaderUtils.getRenderLayer(texture); RenderType layer = ShaderUtils.getRenderLayer(texture);
final float fr = r, fg = g, fb = b; final float fr = r, fg = g, fb = b;
final Obj finalObj = groupObj; final Obj finalObj = groupObj;
//? if >=1.21.9 { //? if >=1.21.9 {
output.submitCustom(matrices, layer, output.submitCustomGeometry(matrices, layer,
(entry, consumer) -> drawGroup(finalObj, consumer, entry, light, overlay, fr, fg, fb)); (entry, consumer) -> drawGroup(finalObj, consumer, entry, light, overlay, fr, fg, fb));
//?} else //?} else
//drawGroup(finalObj, output.getBuffer(layer), matrices.peek(), light, overlay, fr, fg, fb); //drawGroup(finalObj, output.getBuffer(layer), matrices.last(), light, overlay, fr, fg, fb);
} }
} }
private static void drawGroup(Obj obj, VertexConsumer consumer, MatrixStack.Entry entry, int light, int overlay, private static void drawGroup(Obj obj, VertexConsumer consumer, PoseStack.Pose entry, int light, int overlay,
float r, float g, float b) { float r, float g, float b) {
int faces = obj.getNumFaces(); int faces = obj.getNumFaces();
@@ -93,7 +96,7 @@ public class ObjRenderer {
} }
private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer, private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
MatrixStack.Entry matrix, int light, int overlay, PoseStack.Pose matrix, int light, int overlay,
float r, float g, float b) { float r, float g, float b) {
FloatTuple pos = obj.getVertex(face.getVertexIndex(index)); FloatTuple pos = obj.getVertex(face.getVertexIndex(index));
@@ -118,11 +121,11 @@ public class ObjRenderer {
v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left
} }
consumer.vertex(matrix, x, y, z) consumer.addVertex(matrix, x, y, z)
.color(r, g, b, 1f) .setColor(r, g, b, 1f)
.texture(u, v) .setUv(u, v)
.overlay(overlay) .setOverlay(overlay)
.light(light) .setLight(light)
.normal(matrix, nx, ny, nz); .setNormal(matrix, nx, ny, nz);
} }
} }
@@ -3,9 +3,9 @@ package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.texture.NativeImage; import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt; import java.awt.image.DataBufferInt;
@@ -28,7 +28,7 @@ public class IdentifierUtils {
int index = 0; int index = 0;
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, pixels[index++]); nativeImage.setPixel(x, y, pixels[index++]);
} }
} }
@@ -36,18 +36,21 @@ public class IdentifierUtils {
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, bufferedImage.getRGB(x, y)); nativeImage.setPixel(x, y, bufferedImage.getRGB(x, y));
} }
} }
} }
SaturnClient.client.execute(() -> { SaturnClient.client.execute(() -> {
NativeImageBackedTexture texture = new NativeImageBackedTexture( //? if >=1.21.5 {
DynamicTexture texture = new DynamicTexture(
() -> identifier.toString(), () -> identifier.toString(),
nativeImage); nativeImage);
//?} else
//DynamicTexture texture = new DynamicTexture(nativeImage);
SaturnClient.client.getTextureManager().registerTexture(identifier, texture); SaturnClient.client.getTextureManager().register(identifier, texture);
}); });
} catch (Exception e) { } catch (Exception e) {
@@ -3,44 +3,86 @@ package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.cosmetics.utils.GifUtils; import org.saturnclient.cosmetics.utils.GifUtils;
//? if >=1.21.5
import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.pipeline.RenderPipeline;
import net.minecraft.client.gl.RenderPipelines; //? if >=1.21.5
import net.minecraft.client.render.RenderLayer; import net.minecraft.client.renderer.RenderPipelines;
//? if >=1.21.11 { //? if >=1.21.11 {
import net.minecraft.client.render.RenderLayers; import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.render.RenderSetup; //?} else
import net.minecraft.client.render.block.entity.AbstractEndPortalBlockEntityRenderer; //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 { //?} else {
/*import net.minecraft.client.render.RenderPhase.Textures; /*import net.minecraft.client.renderer.RenderStateShard.MultiTextureStateShard;
import net.minecraft.client.render.block.entity.EndPortalBlockEntityRenderer; 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;
*///?} *///?}
import net.minecraft.util.Identifier;
public class ShaderUtils { 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( public static final RenderPipeline END_GATEWAY_PIPELINE = RenderPipelines.register(
RenderPipeline.builder(new RenderPipeline.Snippet[] { RenderPipeline.builder(new RenderPipeline.Snippet[] {
RenderPipelines.RENDERTYPE_END_PORTAL_SNIPPET }) RenderPipelines.END_PORTAL_SNIPPET })
.withLocation("pipeline/end_gateway") .withLocation("pipeline/end_gateway")
.withShaderDefine("PORTAL_LAYERS", 16) .withShaderDefine("PORTAL_LAYERS", 16)
.withCull(false) .withCull(false)
.build()); .build());
//?}
//? if >=1.21.11 { //? if >=1.21.11 {
public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway", public static final RenderType END_GATEWAY = RenderType.create("end_gateway",
RenderSetup.builder(END_GATEWAY_PIPELINE) RenderSetup.builder(END_GATEWAY_PIPELINE)
.texture("Sampler0", AbstractEndPortalBlockEntityRenderer.SKY_TEXTURE) .withTexture("Sampler0", AbstractEndPortalRenderer.END_SKY_LOCATION)
.texture("Sampler1", AbstractEndPortalBlockEntityRenderer.PORTAL_TEXTURE).build()); .withTexture("Sampler1", AbstractEndPortalRenderer.END_PORTAL_LOCATION).createRenderSetup());
//?} else { //?} else if >=1.21.6 {
/*public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway", 1536, false, false, /*public static final RenderType END_GATEWAY = RenderType.create("end_gateway", 1536, false, false,
END_GATEWAY_PIPELINE, END_GATEWAY_PIPELINE,
RenderLayer.MultiPhaseParameters.builder() RenderType.CompositeState.builder()
.texture(Textures.create().add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false) .setTextureState(MultiTextureStateShard.builder().add(TheEndPortalRenderer.END_SKY_LOCATION, false)
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false).build()) .add(TheEndPortalRenderer.END_PORTAL_LOCATION, false).build())
.build(false)); .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 RenderLayer getRenderLayer(IdentifierRef texture) { public static RenderType getRenderLayer(IdentifierRef texture) {
String path = texture.toString(); String path = texture.toString();
if (path.endsWith(".gif")) { if (path.endsWith(".gif")) {
return entityAlpha((Identifier) (Object) GifUtils.get(texture)); return entityAlpha((Identifier) (Object) GifUtils.get(texture));
@@ -51,10 +93,32 @@ public class ShaderUtils {
} }
} }
private static RenderLayer entityAlpha(Identifier texture) { // Cosmetics are drawn unlit, without the lightmap or the directional shading, as the dragon's
//? if >=1.21.11 { // alpha pipeline drew them before 26.1. 26.1 dropped that pipeline, so this is the entity shader
return RenderLayers.entityAlpha(texture); // 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 //?} else
//return RenderLayer.getEntityAlpha(texture); //return RenderType.dragonExplosionAlpha(texture);
} }
} }
@@ -2,32 +2,31 @@ package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature; import org.saturnclient.common.feature.EntityFeature;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.util.hit.EntityHitResult; import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.util.hit.HitResult; import net.minecraft.world.phys.HitResult;
import net.minecraft.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
/** /**
* Fabric implementation of {@link EntityFeature}. * Fabric implementation of {@link EntityFeature}.
* *
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}. * Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* Entity state for nametag rendering is supplied at render time by * Entity state for nametags is built by LivingEntityRendererMixin, which
* {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which * writes health into the render state and then constructs a
* writes into the render state; {@link NametagsMixin} then constructs * {@link RenderStateEntityState} from that snapshot for NametagsMod.
* a {@link RenderStateEntityState} from the already-extracted snapshot.
*/ */
public class EntityFeatureImpl implements EntityFeature { public class EntityFeatureImpl implements EntityFeature {
private final MinecraftClient mc; private final Minecraft mc;
public EntityFeatureImpl(MinecraftClient mc) { public EntityFeatureImpl(Minecraft mc) {
this.mc = mc; this.mc = mc;
} }
@Override @Override
public boolean isTargetingLivingEntity() { public boolean isTargetingLivingEntity() {
HitResult target = mc.crosshairTarget; HitResult target = mc.hitResult;
if (target == null || target.getType() != HitResult.Type.ENTITY) if (target == null || target.getType() != HitResult.Type.ENTITY)
return false; return false;
Entity hit = ((EntityHitResult) target).getEntity(); Entity hit = ((EntityHitResult) target).getEntity();
@@ -43,7 +42,7 @@ public class EntityFeatureImpl implements EntityFeature {
* Minecraft render-state object) as a platform-neutral * Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}. * {@link EntityState} for use by {@link NametagsFeature}.
* *
* This is constructed inside {@link NametagsMixin} where both the * This is constructed in LivingEntityRendererMixin, where both the
* render-state mixin interface and the entity name are available. * render-state mixin interface and the entity name are available.
*/ */
public static final class RenderStateEntityState implements EntityState { public static final class RenderStateEntityState implements EntityState {
@@ -7,8 +7,8 @@ import org.saturnclient.common.feature.EntityFeature;
* {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}. * {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
* *
* Carries the entity's health snapshot and classification into the render * Carries the entity's health snapshot and classification into the render
* pipeline so that {@link NametagsMixin} can read them without touching * render state, so NametagsMod's replacement nametag (applied in
* the live entity again. * LivingEntityRendererMixin) can use them without touching the live entity again.
*/ */
public interface HealthRenderState { public interface HealthRenderState {
@@ -1,6 +1,7 @@
package org.saturnclient.impl.features.mixins.entity; package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity; import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.impl.features.render.ZoomFov;
import org.saturnclient.mod.mods.FreelookMod; import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@@ -9,14 +10,16 @@ import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
//? if >=26.1
//import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.render.Camera; import net.minecraft.client.Camera;
import net.minecraft.entity.Entity; import net.minecraft.world.entity.Entity;
//? if >=1.21.11 { //? if >=1.21.11 {
import net.minecraft.world.World; import net.minecraft.world.level.Level;
//?} else //?} else
//import net.minecraft.world.BlockView; //import net.minecraft.world.level.BlockGetter;
@Mixin(Camera.class) @Mixin(Camera.class)
public abstract class CameraMixin { public abstract class CameraMixin {
@@ -26,30 +29,51 @@ public abstract class CameraMixin {
@Shadow @Shadow
protected abstract void setRotation(float yaw, float pitch); 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)) // Freelook keeps the camera's rotation after vanilla points it along the entity's view. From 26.1
public void lockRotation( // that happens in alignWithEntity, which takes the camera's own entity.
//? if >=1.21.11 { //? if >=26.1 {
World focusedBlock, /*@Shadow
//?} else public abstract Entity entity();
//BlockView focusedBlock,
Entity cameraEntity, @Inject(method = "alignWithEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
boolean isThirdPerson, public void lockRotation(float tickDelta, CallbackInfo ci) {
boolean isFrontFacing, saturn$lockRotation(entity());
float tickDelta, }
CallbackInfo ci) {
if (FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) { // 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; CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && SaturnClient.client.player != null) { if (firstTime && SaturnClient.client.player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getPitch()); cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getXRot());
cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYaw()); cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYRot());
firstTime = false; firstTime = false;
} }
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(), this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(),
cameraOverriddenEntity.freelook$getCameraPitch()); cameraOverriddenEntity.freelook$getCameraPitch());
} }
if (!FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) { if (!FreelookMod.isFreeLooking() && cameraEntity instanceof LocalPlayer) {
firstTime = true; firstTime = true;
} }
} }
@@ -8,9 +8,9 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.Mth;
@Mixin(Entity.class) @Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity { public class EntityMixin implements CameraOverriddenEntity {
@@ -20,15 +20,15 @@ public class EntityMixin implements CameraOverriddenEntity {
@Unique @Unique
private float cameraYaw; private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true) @Inject(method = "turn", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) { public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
// noinspection ConstantValue// IntelliJ is incorrect here, this code block is // noinspection ConstantValue// IntelliJ is incorrect here, this code block is
// reachable // reachable
if (FreelookMod.isFreeLooking() && (Object) this instanceof ClientPlayerEntity) { if (FreelookMod.isFreeLooking() && (Object) this instanceof LocalPlayer) {
double pitchDelta = (yDelta * 0.15); double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15); double yawDelta = (xDelta * 0.15);
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f); this.cameraPitch = Mth.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta; this.cameraYaw += (float) yawDelta;
ci.cancel(); ci.cancel();
@@ -1,101 +0,0 @@
package org.saturnclient.impl.features.mixins.misc;
import net.minecraft.client.render.entity.EntityRenderer;
import org.spongepowered.asm.mixin.Mixin;
//? if <1.21.9 {
/*import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.render.LivingEntityRenderStateMixin;
import org.saturnclient.mod.mods.NametagsMod;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
*///?}
/**
* Intercepts nametag rendering and replaces the displayed text with
* the string produced by {@link NametagsMod#getNametagString}.
*
* Key changes from the original:
*
* <ul>
* <li>The old {@code NametagsFabric.EntityStateImpl} bridge class is
* replaced by {@link EntityFeatureImpl.RenderStateEntityState},
* which wraps the {@link HealthRenderState} already injected into
* the render state by {@link LivingEntityRenderStateMixin}.</li>
* <li>The feature import changes from {@code Nametags} to
* {@link NametagsMod}; the two static methods
* ({@code shouldReplaceName} / {@code getNametagString}) are
* unchanged in signature.</li>
* <li>The entity's display name is extracted from the render state's
* {@code nameVisible} / text fields where available, falling back
* to the plain text string passed to this method.</li>
* </ul>
*
* The re-entrancy guard ({@code saturn$rendering}) is kept intact to
* prevent infinite recursion when we call {@code renderLabelIfPresent}
* with the replacement text.
*/
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
// TODO: 1.21.9 moved label rendering to the render queue (renderLabelIfPresent no longer takes a
// VertexConsumerProvider), so custom nametags only work on older versions. On 1.21.9+ this mixin is
// empty but stays registered, so features.mixins.json is the same for every version.
//? if <1.21.9 {
/*// Guards against the recursive call we make with the replacement text.
@Unique
private static final ThreadLocal<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
@Shadow
protected abstract <S extends EntityRenderState> void renderLabelIfPresent(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light);
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
private <S extends EntityRenderState> void saturn$replaceNametag(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light, CallbackInfo ci) {
// Skip our own recursive call
if (saturn$rendering.get())
return;
if (!NametagsMod.shouldReplaceName())
return;
// Only living entities carry health data
if (!(state instanceof LivingEntityRenderState living))
return;
if (!(living instanceof HealthRenderState hrs))
return;
// Build the platform-neutral EntityState from the render-state snapshot
// (health/type were written by LivingEntityRendererMixin at extract time).
String customName = text != null ? text.getString() : null;
EntityFeatureImpl.RenderStateEntityState entityState = new EntityFeatureImpl.RenderStateEntityState(
customName, hrs);
// Ask the feature for the replacement string
String replacement = NametagsMod.getNametagString(entityState);
if (replacement == null)
return; // feature decided not to replace this entity
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
*///?}
}
@@ -1,6 +1,9 @@
package org.saturnclient.impl.features.mixins.render; package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.fog.FogRenderer; //? 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.saturnclient.mod.mods.NoFogMod;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@@ -10,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(FogRenderer.class) @Mixin(FogRenderer.class)
public class BackgroundRendererMixin { public class BackgroundRendererMixin {
// fog // fog
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true) @ModifyVariable(method = "setupFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) { private static float modifyFogStart(float viewDistance) {
if (NoFogMod.isActive()) { if (NoFogMod.isActive()) {
return viewDistance * 2.0f; return viewDistance * 2.0f;
@@ -1,19 +1,29 @@
package org.saturnclient.impl.features.mixins.render; package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.GameRenderer; //? if >=1.21.6
import net.minecraft.client.Minecraft;
//? if >=1.21.6
import net.minecraft.client.Options;
import net.minecraft.client.renderer.GameRenderer;
import org.saturnclient.mod.mods.ZoomMod; //? 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.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; 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; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** /**
* Overrides the game's field-of-view when {@link ZoomMod} is active. * 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 * The old code checked both {@code Zoom.isZooming} and
* {@code Zoom.shouldZoom()} separately. The refactored * {@code Zoom.shouldZoom()} separately. The refactored
* {@link ZoomMod#shouldZoom()} already ORs both conditions * {@link org.saturnclient.mod.mods.ZoomMod#shouldZoom()} already ORs both conditions
* ({@code enabled.value && isZooming}), so only one check is needed. * ({@code enabled.value && isZooming}), so only one check is needed.
* *
* No {@link org.saturnclient.common.module.ModProvider} is required * No {@link org.saturnclient.common.module.ModProvider} is required
@@ -27,11 +37,29 @@ public class GameRendererMixin {
* After the vanilla FOV has been computed, divide it by the zoom * After the vanilla FOV has been computed, divide it by the zoom
* level to produce the narrowed field-of-view. * 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) @Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) { private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (ZoomMod.shouldZoom()) { cir.setReturnValue(ZoomFov.apply(cir.getReturnValue()));
float zoomedFov = cir.getReturnValue() / ZoomMod.getZoomLevel();
cir.setReturnValue(zoomedFov);
} }
//?}
//? if >=1.21.6 {
// From 1.21.6 the menu blur's strength is the vanilla setting, read here every frame. While a
// Saturn screen is open, it's Saturn's background blur instead, faded in with the screen.
// 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();
}
//?}
} }
@@ -3,7 +3,10 @@ package org.saturnclient.impl.features.mixins.render;
// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils // Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils
import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager; //? if >=26.1 {
/*import net.minecraft.client.renderer.LightmapRenderStateExtractor;
*///?} else
import net.minecraft.client.renderer.LightTexture;
import org.saturnclient.mod.mods.FullbrightMod; import org.saturnclient.mod.mods.FullbrightMod;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@@ -18,14 +21,21 @@ import org.spongepowered.asm.mixin.injection.At;
* it is now {@link FullbrightMod}. The injection target and * it is now {@link FullbrightMod}. The injection target and
* strategy are unchanged. * strategy are unchanged.
*/ */
@Mixin(LightmapTextureManager.class) // 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 { public class LightmapTextureManagerMixin {
/** /**
* If fullbright is active, substitute the feature's brightness * If fullbright is active, substitute the feature's brightness
* value for whatever vanilla calculated. * value for whatever vanilla calculated.
*/ */
@ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)) //? 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) { private float modifyGamma(float original) {
if (FullbrightMod.shouldOverrideBrightness()) { if (FullbrightMod.shouldOverrideBrightness()) {
return FullbrightMod.getBrightnessValue(); return FullbrightMod.getBrightnessValue();
@@ -1,10 +1,9 @@
package org.saturnclient.impl.features.mixins.render; package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.state.LivingEntityRenderState; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature; import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState; import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.misc.NametagsMixin;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
@@ -19,8 +18,8 @@ import org.spongepowered.asm.mixin.Unique;
* *
* The stored values are written by * The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a * {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by * render state is extracted from a live entity, and read there right away for
* {@link NametagsMixin} at render time. * NametagsMod's replacement nametag.
*/ */
@Mixin(LivingEntityRenderState.class) @Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState { public class LivingEntityRenderStateMixin implements HealthRenderState {
@@ -1,34 +1,35 @@
package org.saturnclient.impl.features.mixins.render; package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import net.minecraft.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.entity.mob.HostileEntity; import net.minecraft.world.entity.monster.Monster;
import net.minecraft.entity.passive.PassiveEntity; import net.minecraft.world.entity.AgeableMob;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.world.entity.player.Player;
import net.minecraft.network.chat.Component;
import org.saturnclient.common.feature.EntityFeature; import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.entity.HealthRenderState; import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.mod.mods.NametagsMod;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** /**
* Captures each living entity's health and type into the render state * Captures each living entity's health and type into the render state during
* during {@code updateRenderState}, so that nametag rendering can read * {@code updateRenderState}, and applies {@link NametagsMod}'s replacement nametag to the state's
* them without touching the live entity again. * display name.
* *
* Previously imported {@code NametagsInterface.EntityType}; now uses * Replacing the name here, while the render state is built, works the same on every version. Hooking
* {@link EntityFeature.EntityType} — the canonical enum shared across * label rendering instead doesn't: 1.21.9 moved it to the render command queue. Player role icons are
* the whole module system. * added after this, in PlayerEntityRendererMixin.
*
* The injection target and strategy are unchanged from the original.
*/ */
@Mixin(LivingEntityRenderer.class) @Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> { public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
@Inject(method = "updateRenderState", at = @At("TAIL")) @Inject(method = "extractRenderState", at = @At("TAIL"))
private void saturn$captureHealth(T entity, S state, float tickDelta, CallbackInfo ci) { private void saturn$captureHealth(T entity, S state, float tickDelta, CallbackInfo ci) {
if (!(state instanceof HealthRenderState hrs)) if (!(state instanceof HealthRenderState hrs))
return; return;
@@ -37,15 +38,25 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
// Classify entity type for the nametag feature's filtering logic // Classify entity type for the nametag feature's filtering logic
EntityFeature.EntityType type; EntityFeature.EntityType type;
if (entity instanceof PlayerEntity) if (entity instanceof Player)
type = EntityFeature.EntityType.PLAYER; type = EntityFeature.EntityType.PLAYER;
else if (entity instanceof HostileEntity) else if (entity instanceof Monster)
type = EntityFeature.EntityType.HOSTILE; type = EntityFeature.EntityType.HOSTILE;
else if (entity instanceof PassiveEntity) else if (entity instanceof AgeableMob)
type = EntityFeature.EntityType.PASSIVE; type = EntityFeature.EntityType.PASSIVE;
else else
type = EntityFeature.EntityType.OTHER; type = EntityFeature.EntityType.OTHER;
hrs.saturn$setEntityType(type); 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);
}
}
} }
} }
@@ -1,12 +1,16 @@
package org.saturnclient.impl.features.mixins.render; package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.hud.InGameHud; //? if >=26.2 {
import net.minecraft.client.render.RenderTickCounter; /*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.RenderScopeImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric; import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.mod.HudMod; import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod; import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout; import org.saturnclient.mod.ModLayout;
@@ -37,18 +41,25 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is * {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
* unchanged infrastructure. * unchanged infrastructure.
*/ */
@Mixin(InGameHud.class) // 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 { public class RenderMixin {
//? if >=26.1 {
/*@Inject(method = "extractChat", at = @At("TAIL"))
*///?} else
@Inject(method = "renderChat", at = @At("TAIL")) @Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { public void renderMods(GuiGraphics context, DeltaTracker tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer; Font textRenderer = SaturnClient.client.font;
// Skip rendering while the HUD editor is open so the editor // Skip rendering while the HUD editor is open so the editor
// can draw its own preview layout without double-drawing. // can draw its own preview layout without double-drawing.
if (textRenderer == null) if (textRenderer == null)
return; return;
if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f if (Screens.current() instanceof SaturnScreenFabric f
&& f.screen instanceof HudEditor) && f.screen instanceof HudEditor)
return; return;
@@ -1,7 +1,7 @@
package org.saturnclient.impl.features.mixins.world; package org.saturnclient.impl.features.mixins.world;
import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket; import net.minecraft.network.protocol.game.ClientboundSetTimePacket;
import org.saturnclient.mod.mods.TpsMod; import org.saturnclient.mod.mods.TpsMod;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@@ -20,11 +20,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
* The only change from the original is renaming the import from * The only change from the original is renaming the import from
* {@code feature.features.Tps} to {@link TpsMod}. * {@code feature.features.Tps} to {@link TpsMod}.
*/ */
@Mixin(ClientPlayNetworkHandler.class) @Mixin(ClientPacketListener.class)
public class TpsMixin { public class TpsMixin {
@Inject(method = "onWorldTimeUpdate", at = @At("HEAD")) @Inject(method = "handleSetTime", at = @At("HEAD"))
private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) { private void onWorldTimeUpdate(ClientboundSetTimePacket packet, CallbackInfo ci) {
TpsMod.onTimePacket(packet.time()); TpsMod.onTimePacket(packet.gameTime());
} }
} }
@@ -2,8 +2,8 @@ package org.saturnclient.impl.features.network;
import org.saturnclient.common.feature.NetworkFeature; import org.saturnclient.common.feature.NetworkFeature;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.network.PlayerListEntry; import net.minecraft.client.multiplayer.PlayerInfo;
/** /**
* Fabric implementation of {@link NetworkFeature}. * Fabric implementation of {@link NetworkFeature}.
@@ -14,23 +14,23 @@ import net.minecraft.client.network.PlayerListEntry;
*/ */
public class NetworkFeatureImpl implements NetworkFeature { public class NetworkFeatureImpl implements NetworkFeature {
private final MinecraftClient mc; private final Minecraft mc;
public NetworkFeatureImpl(MinecraftClient mc) { public NetworkFeatureImpl(Minecraft mc) {
this.mc = mc; this.mc = mc;
} }
@Override @Override
public boolean hasNetwork() { public boolean hasNetwork() {
return mc.getNetworkHandler() != null; return mc.getConnection() != null;
} }
@Override @Override
public int getPing() { public int getPing() {
if (mc.player == null || mc.getNetworkHandler() == null) if (mc.player == null || mc.getConnection() == null)
return -1; return -1;
PlayerListEntry entry = mc.getNetworkHandler().getPlayerListEntry(mc.player.getUuid()); PlayerInfo entry = mc.getConnection().getPlayerInfo(mc.player.getUUID());
return entry != null ? entry.getLatency() : -1; return entry != null ? entry.getLatency() : -1;
} }
@@ -1,13 +1,13 @@
package org.saturnclient.impl.features.player; package org.saturnclient.impl.features.player;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.entity.EquipmentSlot; import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.entity.effect.StatusEffectInstance; import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.item.Items; import net.minecraft.world.item.Items;
import net.minecraft.registry.Registries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
import org.saturnclient.common.feature.PlayerFeature; import org.saturnclient.common.feature.PlayerFeature;
import org.saturnclient.common.ref.game.EffectRef; import org.saturnclient.common.ref.game.EffectRef;
@@ -29,9 +29,9 @@ import java.util.List;
*/ */
public class PlayerFeatureImpl implements PlayerFeature { public class PlayerFeatureImpl implements PlayerFeature {
private final MinecraftClient mc; private final Minecraft mc;
public PlayerFeatureImpl(MinecraftClient mc) { public PlayerFeatureImpl(Minecraft mc) {
this.mc = mc; this.mc = mc;
} }
@@ -40,7 +40,7 @@ public class PlayerFeatureImpl implements PlayerFeature {
// --------------------------------------------------------------- // ---------------------------------------------------------------
/** Returns the local player, or {@code null} if one is not loaded. */ /** Returns the local player, or {@code null} if one is not loaded. */
private ClientPlayerEntity player() { private LocalPlayer player() {
return mc.player; return mc.player;
} }
@@ -78,42 +78,42 @@ public class PlayerFeatureImpl implements PlayerFeature {
@Override @Override
public boolean isForwardPressed() { public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed(); return mc.options.keyUp.isDown();
} }
@Override @Override
public boolean isBackPressed() { public boolean isBackPressed() {
return mc.options.backKey.isPressed(); return mc.options.keyDown.isDown();
} }
@Override @Override
public boolean isLeftPressed() { public boolean isLeftPressed() {
return mc.options.leftKey.isPressed(); return mc.options.keyLeft.isDown();
} }
@Override @Override
public boolean isRightPressed() { public boolean isRightPressed() {
return mc.options.rightKey.isPressed(); return mc.options.keyRight.isDown();
} }
@Override @Override
public boolean isJumpPressed() { public boolean isJumpPressed() {
return mc.options.jumpKey.isPressed(); return mc.options.keyJump.isDown();
} }
@Override @Override
public boolean isAttackPressed() { public boolean isAttackPressed() {
return mc.options.attackKey.isPressed(); return mc.options.keyAttack.isDown();
} }
@Override @Override
public boolean isUsePressed() { public boolean isUsePressed() {
return mc.options.useKey.isPressed(); return mc.options.keyUse.isDown();
} }
@Override @Override
public boolean isSneaking() { public boolean isSneaking() {
return hasPlayer() && player().isSneaking(); return hasPlayer() && player().isShiftKeyDown();
} }
@Override @Override
@@ -128,7 +128,7 @@ public class PlayerFeatureImpl implements PlayerFeature {
@Override @Override
public boolean isOnGround() { public boolean isOnGround() {
return hasPlayer() && player().isOnGround(); return hasPlayer() && player().onGround();
} }
@Override @Override
@@ -145,7 +145,7 @@ public class PlayerFeatureImpl implements PlayerFeature {
public Velocity getVelocity() { public Velocity getVelocity() {
if (!hasPlayer()) if (!hasPlayer())
return Velocity.ZERO; return Velocity.ZERO;
var v = player().getVelocity(); var v = player().getDeltaMovement();
return new Velocity(v.x, v.y, v.z); return new Velocity(v.x, v.y, v.z);
} }
@@ -164,27 +164,39 @@ public class PlayerFeatureImpl implements PlayerFeature {
@Override @Override
public ItemStackRef getMainHand() { public ItemStackRef getMainHand() {
return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY); return wrap(hasPlayer() ? player().getMainHandItem() : ItemStack.EMPTY);
} }
@Override @Override
public ItemStackRef getHelmet() { public ItemStackRef getHelmet() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.HEAD) : ItemStack.EMPTY); //? 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 @Override
public ItemStackRef getChestplate() { public ItemStackRef getChestplate() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.CHEST) : ItemStack.EMPTY); //? 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 @Override
public ItemStackRef getLeggings() { public ItemStackRef getLeggings() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.LEGS) : ItemStack.EMPTY); //? 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 @Override
public ItemStackRef getBoots() { public ItemStackRef getBoots() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.FEET) : ItemStack.EMPTY); //? if >=1.21.5 {
return wrap(hasPlayer() ? player().getItemBySlot(EquipmentSlot.FEET) : ItemStack.EMPTY);
//?} else
//return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -225,9 +237,9 @@ public class PlayerFeatureImpl implements PlayerFeature {
if (!hasPlayer()) if (!hasPlayer())
return Collections.emptyList(); return Collections.emptyList();
var effects = player().getActiveStatusEffects().values(); var effects = player().getActiveEffectsMap().values();
List<EffectRef> result = new ArrayList<>(effects.size()); List<EffectRef> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) { for (MobEffectInstance instance : effects) {
result.add((EffectRef) instance); result.add((EffectRef) instance);
} }
return result; return result;
@@ -236,12 +248,12 @@ public class PlayerFeatureImpl implements PlayerFeature {
@Override @Override
public List<? extends EffectRef> getDummyEffects() { public List<? extends EffectRef> getDummyEffects() {
return List.of( return List.of(
(EffectRef) new StatusEffectInstance( (EffectRef) new MobEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2), BuiltInRegistries.MOB_EFFECT.get(Identifier.withDefaultNamespace("speed")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance( (EffectRef) new MobEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2), BuiltInRegistries.MOB_EFFECT.get(Identifier.withDefaultNamespace("strength")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance( (EffectRef) new MobEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2)); BuiltInRegistries.MOB_EFFECT.get(Identifier.withDefaultNamespace("fire_resistance")).get(), 12000, 2));
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -2,8 +2,8 @@ package org.saturnclient.impl.features.render;
import org.saturnclient.common.feature.RenderFeature; import org.saturnclient.common.feature.RenderFeature;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.option.Perspective; import net.minecraft.client.CameraType;
/** /**
* Fabric implementation of {@link RenderFeature}. * Fabric implementation of {@link RenderFeature}.
@@ -14,9 +14,9 @@ import net.minecraft.client.option.Perspective;
*/ */
public class RenderFeatureImpl implements RenderFeature { public class RenderFeatureImpl implements RenderFeature {
private final MinecraftClient mc; private final Minecraft mc;
public RenderFeatureImpl(MinecraftClient mc) { public RenderFeatureImpl(Minecraft mc) {
this.mc = mc; this.mc = mc;
} }
@@ -26,12 +26,12 @@ public class RenderFeatureImpl implements RenderFeature {
@Override @Override
public int getScaledWindowWidth() { public int getScaledWindowWidth() {
return mc.getWindow().getScaledWidth(); return mc.getWindow().getGuiScaledWidth();
} }
@Override @Override
public int getScaledWindowHeight() { public int getScaledWindowHeight() {
return mc.getWindow().getScaledHeight(); return mc.getWindow().getGuiScaledHeight();
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -40,7 +40,7 @@ public class RenderFeatureImpl implements RenderFeature {
@Override @Override
public int getFps() { public int getFps() {
return mc.getCurrentFps(); return mc.getFps();
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -49,16 +49,16 @@ public class RenderFeatureImpl implements RenderFeature {
@Override @Override
public boolean isFirstPerson() { public boolean isFirstPerson() {
return mc.options.getPerspective() == Perspective.FIRST_PERSON; return mc.options.getCameraType() == CameraType.FIRST_PERSON;
} }
@Override @Override
public void setFirstPerson() { public void setFirstPerson() {
mc.options.setPerspective(Perspective.FIRST_PERSON); mc.options.setCameraType(CameraType.FIRST_PERSON);
} }
@Override @Override
public void setThirdPersonBack() { public void setThirdPersonBack() {
mc.options.setPerspective(Perspective.THIRD_PERSON_BACK); 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;
}
}
@@ -2,7 +2,7 @@ package org.saturnclient.impl.features.world;
import org.saturnclient.common.feature.WorldFeature; import org.saturnclient.common.feature.WorldFeature;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
/** /**
* Fabric implementation of {@link WorldFeature}. * Fabric implementation of {@link WorldFeature}.
@@ -12,15 +12,15 @@ import net.minecraft.client.MinecraftClient;
*/ */
public class WorldFeatureImpl implements WorldFeature { public class WorldFeatureImpl implements WorldFeature {
private final MinecraftClient mc; private final Minecraft mc;
public WorldFeatureImpl(MinecraftClient mc) { public WorldFeatureImpl(Minecraft mc) {
this.mc = mc; this.mc = mc;
} }
@Override @Override
public boolean hasWorld() { public boolean hasWorld() {
return mc.world != null; return mc.level != null;
} }
/** /**
@@ -31,10 +31,7 @@ public class WorldFeatureImpl implements WorldFeature {
*/ */
@Override @Override
public long getWorldAge() { public long getWorldAge() {
//? if >=1.21.11 { // World.getTime() was a shortcut for this, and 1.21.11 removed it.
// TODO: not implemented on 1.21.11 yet. return hasWorld() ? mc.level.getLevelData().getGameTime() : 0L;
return 0L;
//?} else
//return hasWorld() ? mc.world.getTime() : 0L;
} }
} }
@@ -4,7 +4,7 @@ import org.saturnclient.common.ref.asset.IdentifierRef;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
@Mixin(Identifier.class) @Mixin(Identifier.class)
public abstract class IdentifierMixin implements IdentifierRef { public abstract class IdentifierMixin implements IdentifierRef {
@@ -4,7 +4,7 @@ import org.saturnclient.common.ref.game.ItemStackRef;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.item.ItemStack; import net.minecraft.world.item.ItemStack;
@Mixin(ItemStack.class) @Mixin(ItemStack.class)
public abstract class ItemStackMixin implements ItemStackRef { public abstract class ItemStackMixin implements ItemStackRef {
@@ -15,5 +15,21 @@ public abstract class ItemStackMixin implements ItemStackRef {
public abstract int getMaxDamage(); public abstract int getMaxDamage();
@Shadow @Shadow
public abstract int getDamage(); 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 {}
@@ -4,61 +4,61 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.util.UUID; import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.game.MinecraftClientRef; import org.saturnclient.common.ref.game.MinecraftClientRef;
import org.saturnclient.common.ref.render.WindowRef; import org.saturnclient.common.ref.render.WindowRef;
import org.saturnclient.impl.ui.SaturnScreenFabric; import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.ui.SaturnScreen; import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.screens.TitleMenu; import org.saturnclient.ui.screens.TitleMenu;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.gui.screens.options.OptionsScreen;
import net.minecraft.client.gui.screen.option.OptionsScreen; import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
import net.minecraft.client.gui.screen.world.SelectWorldScreen; import net.minecraft.client.Options;
import net.minecraft.client.option.GameOptions; import net.minecraft.client.User;
import net.minecraft.client.session.Session; import com.mojang.blaze3d.platform.Window;
import net.minecraft.client.util.Window; import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraft.resource.ReloadableResourceManagerImpl; import net.minecraft.resources.Identifier;
import net.minecraft.util.Identifier;
@Mixin(MinecraftClient.class) @Mixin(Minecraft.class)
public abstract class MinecraftClientMixin implements MinecraftClientRef { public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Shadow @Shadow
public File runDirectory; public File gameDirectory;
@Shadow @Shadow
private Window window; private Window window;
@Shadow @Shadow
private ReloadableResourceManagerImpl resourceManager; private ReloadableResourceManager resourceManager;
@Shadow @Shadow
public GameOptions options; public Options options;
@Shadow @Shadow
private Session session; private User user;
@Shadow @Shadow
public abstract void setScreen(@Nullable Screen screen); public abstract void stop();
@Shadow
@Override @Override
public abstract void scheduleStop(); public void scheduleStop() {
stop();
}
@Override @Override
public File getRunDirectory() { public File getRunDirectory() {
return runDirectory; return gameDirectory;
} }
@Override @Override
public InputStream getResource(IdentifierRef identifier) { public InputStream getResource(IdentifierRef identifier) {
try { try {
return resourceManager.getResource((Identifier) (Object) identifier).get().getInputStream(); return resourceManager.getResource((Identifier) (Object) identifier).get().open();
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
@@ -71,7 +71,7 @@ public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Override @Override
public void setScreen(SaturnScreen screen) { public void setScreen(SaturnScreen screen) {
this.setScreen(new SaturnScreenFabric(screen)); Screens.set(new SaturnScreenFabric(screen));
} }
@Override @Override
@@ -80,42 +80,48 @@ public abstract class MinecraftClientMixin implements MinecraftClientRef {
switch (screen) { switch (screen) {
case SelectWorld: case SelectWorld:
this.setScreen(new SelectWorldScreen(exitTarget)); Screens.set(new SelectWorldScreen(exitTarget));
break; break;
case Multiplayer: case Multiplayer:
this.setScreen(new MultiplayerScreen(exitTarget)); Screens.set(new JoinMultiplayerScreen(exitTarget));
break; break;
case Options: case Options:
this.setScreen(new OptionsScreen(exitTarget, this.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; break;
} }
} }
@Override @Override
public String getAccessToken() { public String getAccessToken() {
if (session == null) { if (user == null) {
return null; return null;
} }
return this.session.getAccessToken(); return this.user.getAccessToken();
} }
@Override @Override
public UUID getUuid() { public UUID getUuid() {
if (session == null) { if (user == null) {
return null; return null;
} }
return this.session.getUuidOrNull(); return this.user.getProfileId();
} }
@Override @Override
public String getUsername() { public String getUsername() {
if (session == null) { if (user == null) {
return null; return null;
} }
return this.session.getUsername(); return this.user.getName();
} }
@Override @Override
@@ -131,13 +137,13 @@ public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Override @Override
@SuppressWarnings("resource") @SuppressWarnings("resource")
public String getPlayerListEntry(UUID uuid) { public String getPlayerListEntry(UUID uuid) {
var client = (MinecraftClient) (Object) this; var client = (Minecraft) (Object) this;
var handler = client.getNetworkHandler(); var handler = client.getConnection();
if (handler == null) if (handler == null)
return null; return null;
var entry = handler.getPlayerListEntry(uuid); var entry = handler.getPlayerInfo(uuid);
if (entry == null) if (entry == null)
return null; return null;
@@ -150,13 +156,13 @@ public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Override @Override
@SuppressWarnings("resource") @SuppressWarnings("resource")
public UUID getPlayerListEntryUUID(String name) { public UUID getPlayerListEntryUUID(String name) {
var client = (MinecraftClient) (Object) this; var client = (Minecraft) (Object) this;
var handler = client.getNetworkHandler(); var handler = client.getConnection();
if (handler == null) if (handler == null)
return null; return null;
var entry = handler.getPlayerListEntry(name); var entry = handler.getPlayerInfo(name);
if (entry == null) if (entry == null)
return null; return null;
@@ -1,9 +1,9 @@
package org.saturnclient.impl.mixins; package org.saturnclient.impl.mixins;
import net.minecraft.client.texture.Sprite; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.saturnclient.common.ref.asset.SpriteRef; import org.saturnclient.common.ref.asset.SpriteRef;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@Mixin(Sprite.class) @Mixin(TextureAtlasSprite.class)
public abstract class SpriteMixin implements SpriteRef { public abstract class SpriteMixin implements SpriteRef {
} }
@@ -8,42 +8,64 @@ import org.saturnclient.common.ref.game.EffectRef;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.gui.hud.InGameHud; //? if >=26.2 {
import net.minecraft.entity.effect.StatusEffect; /*import net.minecraft.client.gui.Hud;
import net.minecraft.entity.effect.StatusEffectInstance; *///?} else
import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.client.gui.Gui;
import net.minecraft.util.Identifier; import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.core.Holder;
import net.minecraft.resources.Identifier;
@Mixin(StatusEffectInstance.class) @Mixin(MobEffectInstance.class)
public abstract class StatusEffectMixin implements EffectRef { public abstract class StatusEffectMixin implements EffectRef {
@Shadow @Shadow
public abstract boolean shouldShowIcon(); public abstract boolean showIcon();
@Shadow @Shadow
public abstract boolean isInfinite(); public abstract boolean isInfiniteDuration();
@Shadow @Shadow
public abstract int getDuration(); public abstract int getDuration();
@Shadow @Shadow
public abstract RegistryEntry<StatusEffect> getEffectType(); 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 @Override
public SpriteRef getIcon() { public SpriteRef getIcon() {
// 1.21.9 changed the GUI atlas, so newer versions draw the effect's texture (getIconId) instead. // 1.21.9 changed the GUI atlas, so newer versions draw the effect's texture (getIconId) instead.
//? if >=1.21.9 { //? if >=1.21.9 {
return null; return null;
//?} else if >=1.21.6 {
//return (SpriteRef) SaturnClient.client.getGuiSprites().getSprite((Gui.getMobEffectSprite(getEffect())));
//?} else //?} else
//return (SpriteRef) SaturnClient.client.getGuiAtlasManager().getSprite((InGameHud.getEffectTexture(getEffectType()))); //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 @Override
public IdentifierRef getIconId() { public IdentifierRef getIconId() {
Identifier id = InGameHud.getEffectTexture(getEffectType()); //? if >=26.2 {
/*Identifier id = Hud.getMobEffectSprite(getEffect());
*///?} else
Identifier id = Gui.getMobEffectSprite(getEffect());
return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png"); return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png");
} }
//?}
@Override @Override
public String getInfiniteText() { public String getInfiniteText() {
@@ -5,25 +5,25 @@ import org.saturnclient.common.ref.render.TextRef;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.text.Style; import net.minecraft.network.chat.Style;
import net.minecraft.text.Text; import net.minecraft.network.chat.Component;
//? if >=1.21.9 //? if >=1.21.9
import net.minecraft.text.StyleSpriteSource.Font; import net.minecraft.network.chat.FontDescription.Resource;
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
@Mixin(Text.class) @Mixin(Component.class)
public interface TextMixin extends TextRef { public interface TextMixin extends TextRef {
@Override @Override
public default TextRef withFont(IdentifierRef font) { public default TextRef withFont(IdentifierRef font) {
//? if >=1.21.9 { //? if >=1.21.9 {
return (TextRef) (Text) (((Text) this).copy().setStyle(Style.EMPTY.withFont(new Font((Identifier) (Object) font)))); return (TextRef) (Component) (((Component) this).copy().setStyle(Style.EMPTY.withFont(new Resource((Identifier) (Object) font))));
//?} else //?} else
//return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font))); //return (TextRef) (((Component) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
} }
@Override @Override
public default int getWidth() { public default int getWidth() {
return SaturnClient.client.textRenderer.getWidth((Text) this); return SaturnClient.client.font.width((Component) this);
} }
} }
@@ -4,19 +4,41 @@ import org.saturnclient.common.ref.render.WindowRef;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.util.Window; import com.mojang.blaze3d.platform.Window;
@Mixin(Window.class) @Mixin(Window.class)
public abstract class WindowMixin implements WindowRef { public abstract class WindowMixin implements WindowRef {
@Shadow
public abstract int getScreenWidth();
@Shadow
public abstract int getScreenHeight();
// The framebuffer's size
@Shadow @Shadow
public abstract int getWidth(); public abstract int getWidth();
@Shadow @Shadow
public abstract int getHeight(); public abstract int getHeight();
@Shadow // WindowRef's methods delegate to Window's, whose names production renames.
public abstract int getFramebufferWidth(); @Override
public int getWindowWidth() {
return getScreenWidth();
}
@Shadow @Override
public abstract int getFramebufferHeight(); public int getWindowHeight() {
return getScreenHeight();
}
@Override
public int getFramebufferWidth() {
return getWidth();
}
@Override
public int getFramebufferHeight() {
return getHeight();
}
} }
@@ -1,6 +1,6 @@
package org.saturnclient.impl.provider; package org.saturnclient.impl.provider;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import org.saturnclient.common.feature.*; import org.saturnclient.common.feature.*;
import org.saturnclient.common.provider.FeatureProvider; import org.saturnclient.common.provider.FeatureProvider;
@@ -33,7 +33,7 @@ public class FeatureProviderFabric implements FeatureProvider {
private final RenderFeature render; private final RenderFeature render;
private final NetworkFeature network; private final NetworkFeature network;
public FeatureProviderFabric(MinecraftClient mc) { public FeatureProviderFabric(Minecraft mc) {
this.player = new PlayerFeatureImpl(mc); this.player = new PlayerFeatureImpl(mc);
this.world = new WorldFeatureImpl(mc); this.world = new WorldFeatureImpl(mc);
this.entity = new EntityFeatureImpl(mc); this.entity = new EntityFeatureImpl(mc);
@@ -1,18 +1,36 @@
package org.saturnclient.impl.provider; 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.lwjgl.glfw.GLFW;
import org.saturnclient.common.provider.GLFWProvider; import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
public class GLFWProviderImpl implements GLFWProvider { public class GLFWProviderImpl implements GLFWProvider {
@Override @Override
public boolean isKeyPressed(int key) { public boolean isKeyPressed(int key) {
return GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), key) == GLFW.GLFW_PRESS //? if >=26.3 {
&& SaturnClient.client.currentScreen == null; /*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 @Override
public String getKeyName(int key) { 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)); 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;
}
}
@@ -4,22 +4,22 @@ import org.saturnclient.common.provider.RefConstructorProvider;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.render.TextRef; import org.saturnclient.common.ref.render.TextRef;
import net.minecraft.text.Text; import net.minecraft.network.chat.Component;
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
public class RefConstructorImpl implements RefConstructorProvider { public class RefConstructorImpl implements RefConstructorProvider {
@Override @Override
public IdentifierRef identifier(String fullPath) { public IdentifierRef identifier(String fullPath) {
return (IdentifierRef) (Object) Identifier.of(fullPath); return (IdentifierRef) (Object) Identifier.parse(fullPath);
} }
@Override @Override
public IdentifierRef identifier(String namespace, String path) { public IdentifierRef identifier(String namespace, String path) {
return (IdentifierRef) (Object) Identifier.of(namespace, path); return (IdentifierRef) (Object) Identifier.fromNamespaceAndPath(namespace, path);
} }
@Override @Override
public TextRef text(String text) { public TextRef text(String text) {
return (TextRef) (Text) Text.literal(text); return (TextRef) (Component) Component.literal(text);
} }
} }
@@ -2,11 +2,17 @@ package org.saturnclient.impl.provider;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.toast.SystemToast; import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.text.Text; //? 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.provider.SaturnProvider;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
@@ -16,6 +22,9 @@ import org.saturnclient.impl.cosmetics.utils.IdentifierUtils;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
public class SaturnProviderImpl implements SaturnProvider { 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 @Override
public MinecraftClientRef getClient() { public MinecraftClientRef getClient() {
return (MinecraftClientRef) SaturnClient.client; return (MinecraftClientRef) SaturnClient.client;
@@ -48,7 +57,24 @@ public class SaturnProviderImpl implements SaturnProvider {
} }
/** Shown longer than the vanilla default, since these ask the player to act. */ /** Shown longer than the vanilla default, since these ask the player to act. */
private static final SystemToast.Type WARNING_TOAST = new SystemToast.Type(15_000L); 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 @Override
public String getModVersion() { public String getModVersion() {
@@ -59,9 +85,14 @@ public class SaturnProviderImpl implements SaturnProvider {
@Override @Override
public void showWarning(String title, String message) { public void showWarning(String title, String message) {
MinecraftClient client = MinecraftClient.getInstance(); Minecraft client = Minecraft.getInstance();
client.execute(() -> client.getToastManager().add( //? if >=26.2 {
SystemToast.create(client, WARNING_TOAST, Text.literal(title), Text.literal(message)))); /*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 @Override
@@ -1,61 +1,62 @@
package org.saturnclient.impl.ui; 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.jetbrains.annotations.Nullable;
import org.joml.Quaternionf; import org.joml.Quaternionf;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.components.SkinPreview.EntityDrawer; import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
//? if >=1.21.9 { //? if >=1.21.9 {
import net.minecraft.client.render.entity.EntityRenderManager; import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
//?} else //?} else
//import net.minecraft.client.render.entity.EntityRenderDispatcher; //import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.render.entity.EntityRenderer; import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.render.entity.state.EntityRenderState; import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
public class EntityDrawerImpl implements EntityDrawer { public class EntityDrawerImpl implements EntityDrawer {
@Override @Override
public void render(RenderScope renderScope, boolean negativeAngle, float angle, float scale, ElementContext ctx) { public boolean render(RenderScope renderScope, int width, int height, float yaw) {
LivingEntity entity = SaturnClient.client.player; LivingEntity entity = SaturnClient.client.player;
if (entity != null) { if (entity == null) {
drawEntity(renderScope, 0, 0, 75, 78, 30, 0.0625F, ctx.mouseX / scale, ctx.mouseY / scale, return false;
SaturnClient.client.player);
}
} }
// 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, public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float scale,
float mouseX, float mouseY, LivingEntity entity) { float yaw, LivingEntity entity) {
float f = (float) (x1 + x2) / 2.0F;
float g = (float) (y1 + y2) / 2.0F;
renderScope.enableScissor(x1, y1, x2, y2); renderScope.enableScissor(x1, y1, x2, y2);
float h = (float) Math.atan((double) ((f - mouseX) / 40.0F));
float i = (float) Math.atan((double) ((g - mouseY) / 40.0F));
Quaternionf quaternionf = (new Quaternionf()).rotateZ((float) Math.PI); Quaternionf quaternionf = (new Quaternionf()).rotateZ((float) Math.PI);
Quaternionf quaternionf2 = (new Quaternionf()).rotateX(i * 20.0F * ((float) Math.PI / 180F)); Quaternionf quaternionf2 = new Quaternionf();
quaternionf.mul(quaternionf2); float j = entity.yBodyRot;
float j = entity.bodyYaw; float k = entity.getYRot();
float k = entity.getYaw(); float l = entity.getXRot();
float l = entity.getPitch(); float m = entity.yHeadRotO;
float m = entity.lastHeadYaw; float n = entity.yHeadRot;
float n = entity.headYaw; entity.yBodyRot = 180.0F + yaw;
entity.bodyYaw = 180.0F + h * 20.0F; entity.setYRot(180.0F + yaw);
entity.setYaw(180.0F + h * 40.0F); entity.setXRot(0.0F);
entity.setPitch(-i * 20.0F); entity.yHeadRot = entity.getYRot();
entity.headYaw = entity.getYaw(); entity.yHeadRotO = entity.getYRot();
entity.lastHeadYaw = entity.getYaw();
float o = entity.getScale(); float o = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + scale * o, 0.0F); Vector3f vector3f = new Vector3f(0.0F, entity.getBbHeight() / 2.0F + scale * o, 0.0F);
float p = (float) size / o; float p = (float) size / o;
drawEntity(renderScope, x1, y1, x2, y2, p, vector3f, quaternionf, quaternionf2, entity); drawEntity(renderScope, x1, y1, x2, y2, p, vector3f, quaternionf, quaternionf2, entity);
entity.bodyYaw = j; entity.yBodyRot = j;
entity.setYaw(k); entity.setYRot(k);
entity.setPitch(l); entity.setXRot(l);
entity.lastHeadYaw = m; entity.yHeadRotO = m;
entity.headYaw = n; entity.yHeadRot = n;
renderScope.disableScissor(); renderScope.disableScissor();
} }
@@ -63,15 +64,112 @@ public class EntityDrawerImpl implements EntityDrawer {
Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle, Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle,
LivingEntity entity) { LivingEntity entity) {
//? if >=1.21.9 { //? if >=1.21.9 {
EntityRenderManager entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher(); EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
//?} else //?} else
//EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher(); //EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
EntityRenderer<? super LivingEntity, ?> entityRenderer = entityRenderDispatcher.getRenderer(entity); EntityRenderer<? super LivingEntity, ?> entityRenderer = entityRenderDispatcher.getRenderer(entity);
EntityRenderState entityRenderState = entityRenderer.getAndUpdateRenderState(entity, 1.0F); EntityRenderState entityRenderState = entityRenderer.createRenderState(entity, 1.0F);
//? if <1.21.11 //? if <1.21.11
//entityRenderState.hitbox = null; //entityRenderState.hitboxesRenderState = null;
if (renderScope instanceof RenderScopeImpl i) if (renderScope instanceof RenderScopeImpl i)
i.addEntity(entityRenderState, scale, translation, rotation, overrideCameraAngle, x1, y1, x2, y2); 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;
}
}
*///?}
@@ -1,8 +1,13 @@
package org.saturnclient.impl.ui; 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.ArrayDeque;
import java.util.Deque; import java.util.Deque;
import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f; import org.joml.Matrix3x2f;
@@ -23,33 +28,32 @@ import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.SvgTexture; import org.saturnclient.ui.resources.SvgTexture;
import net.minecraft.client.gl.RenderPipelines; import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.ScreenRect; import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.state.ColoredQuadGuiElementRenderState; import net.minecraft.client.gui.render.state.ColoredRectangleRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState; import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.ItemGuiElementRenderState; import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.TextGuiElementRenderState; import net.minecraft.client.gui.render.state.GuiTextRenderState;
import net.minecraft.client.gui.render.state.TexturedQuadGuiElementRenderState; import net.minecraft.client.gui.render.state.BlitRenderState;
import net.minecraft.client.gui.render.state.special.EntityGuiElementRenderState; import net.minecraft.client.gui.render.state.pip.GuiEntityRenderState;
import net.minecraft.client.render.entity.state.EntityRenderState; import net.minecraft.client.renderer.entity.state.EntityRenderState;
//? if >=1.21.7 { //? if >=1.21.7 {
import net.minecraft.client.render.item.KeyedItemRenderState; import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
//?} else //?} else
//import net.minecraft.client.render.item.ItemRenderState; //import net.minecraft.client.renderer.item.ItemStackRenderState;
//? if >=1.21.11 import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.client.texture.AbstractTexture; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.texture.Sprite; import net.minecraft.client.gui.render.TextureSetup;
import net.minecraft.client.texture.TextureSetup; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.entity.LivingEntity; import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.item.ItemDisplayContext; import net.minecraft.world.item.ItemStack;
import net.minecraft.item.ItemStack; import net.minecraft.network.chat.Component;
import net.minecraft.text.Text; import net.minecraft.resources.Identifier;
import net.minecraft.util.Identifier; import net.minecraft.ReportedException;
import net.minecraft.util.crash.CrashException; import net.minecraft.CrashReport;
import net.minecraft.util.crash.CrashReport; import net.minecraft.CrashReportCategory;
import net.minecraft.util.crash.CrashReportSection; import net.minecraft.world.level.Level;
import net.minecraft.world.World;
public class RenderScopeImpl implements RenderScope { public class RenderScopeImpl implements RenderScope {
@@ -58,9 +62,11 @@ public class RenderScopeImpl implements RenderScope {
private ScissorStack scissorStack; private ScissorStack scissorStack;
private int opacity = 0xFF << 24; private int opacity = 0xFF << 24;
// The glass blurs made for this scope's frame
private final Set<Integer> capturedBlurs = new HashSet<>();
public RenderScopeImpl(DrawContext context) { public RenderScopeImpl(GuiGraphics context) {
this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState()); this(context.pose(), ((DrawContextAccessor) (Object) context).getState());
} }
public RenderScopeImpl(Matrix3x2fStack matrices, GuiRenderState state) { public RenderScopeImpl(Matrix3x2fStack matrices, GuiRenderState state) {
@@ -89,10 +95,10 @@ public class RenderScopeImpl implements RenderScope {
@Override @Override
public void fill(int x1, int y1, int x2, int y2, int color) { public void fill(int x1, int y1, int x2, int y2, int color) {
this.state.addSimpleElement( this.state.submitGuiElement(
new ColoredQuadGuiElementRenderState( new ColoredRectangleRenderState(
RenderPipelines.GUI, RenderPipelines.GUI,
TextureSetup.empty(), TextureSetup.noTexture(),
new Matrix3x2f(this.matrices.stack), new Matrix3x2f(this.matrices.stack),
x1, y1, x2, y2, color, color, x1, y1, x2, y2, color, color,
this.scissorStack.peekLast())); this.scissorStack.peekLast()));
@@ -143,10 +149,10 @@ public class RenderScopeImpl implements RenderScope {
matrices.translate(x, y + (lineIndex * Fonts.getHeight())); matrices.translate(x, y + (lineIndex * Fonts.getHeight()));
matrices.scale(scale, scale); matrices.scale(scale, scale);
this.state.addText( this.state.submitText(
new TextGuiElementRenderState( new GuiTextRenderState(
SaturnClient.client.textRenderer, SaturnClient.client.font,
((Text) Fonts.setFont(line, font)).asOrderedText(), ((Component) Fonts.setFont(line, font)).getVisualOrderText(),
new Matrix3x2f(this.matrices.stack), new Matrix3x2f(this.matrices.stack),
0, font == 0 ? 1 : 8, 0, font == 0 ? 1 : 8,
//? if >=1.21.11 { //? if >=1.21.11 {
@@ -162,12 +168,12 @@ public class RenderScopeImpl implements RenderScope {
@Override @Override
public int getScaledWindowWidth() { public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getScaledWidth(); return SaturnClient.client.getWindow().getGuiScaledWidth();
} }
@Override @Override
public int getScaledWindowHeight() { public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight(); return SaturnClient.client.getWindow().getGuiScaledHeight();
} }
// Texture rendering // Texture rendering
@@ -211,26 +217,45 @@ public class RenderScopeImpl implements RenderScope {
} }
GpuTextureView gpuTextureView = SaturnClient.client.getTextureManager() GpuTextureView gpuTextureView = SaturnClient.client.getTextureManager()
.getTexture(sprite).getGlTextureView(); .getTexture(sprite).getTextureView();
//? if >=1.21.11 //? if >=1.21.11
AbstractTexture abstractTexture = SaturnClient.client.getTextureManager().getTexture(sprite); AbstractTexture abstractTexture = SaturnClient.client.getTextureManager().getTexture(sprite);
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState( this.state.submitGuiElement(new BlitRenderState(
RenderPipelines.GUI_TEXTURED, RenderPipelines.GUI_TEXTURED,
//? if >=1.21.11 { //? if >=1.21.11 {
TextureSetup.of(gpuTextureView, abstractTexture.getSampler()), TextureSetup.singleTexture(gpuTextureView, abstractTexture.getSampler()),
//?} else //?} else
//TextureSetup.withoutGlTexture(gpuTextureView), //TextureSetup.singleTexture(gpuTextureView),
new Matrix3x2f(this.matrices.stack), new Matrix3x2f(this.matrices.stack),
x1, y1, x2, y2, u1, u2, v1, v2, color, x1, y1, x2, y2, u1, u2, v1, v2, color,
this.scissorStack.peekLast())); 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 // Scissor management
@Override @Override
public void enableScissor(int x1, int y1, int x2, int y2) { public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect rect = new ScreenRect(x1, y1, x2 - x1, y2 - y1).transform(this.matrices.stack); ScreenRectangle rect = new ScreenRectangle(x1, y1, x2 - x1, y2 - y1).transformAxisAligned(this.matrices.stack);
scissorStack.push(rect); scissorStack.push(rect);
} }
@@ -245,12 +270,12 @@ public class RenderScopeImpl implements RenderScope {
} }
public static class ScissorStack { public static class ScissorStack {
private final Deque<ScreenRect> stack = new ArrayDeque<>(); private final Deque<ScreenRectangle> stack = new ArrayDeque<>();
public ScreenRect push(ScreenRect rect) { public ScreenRectangle push(ScreenRectangle rect) {
ScreenRect last = stack.peekLast(); ScreenRectangle last = stack.peekLast();
if (last != null) { if (last != null) {
ScreenRect intersection = Objects.requireNonNullElse(rect.intersection(last), ScreenRect.empty()); ScreenRectangle intersection = Objects.requireNonNullElse(rect.intersection(last), ScreenRectangle.empty());
stack.addLast(intersection); stack.addLast(intersection);
return intersection; return intersection;
} else { } else {
@@ -260,7 +285,7 @@ public class RenderScopeImpl implements RenderScope {
} }
@Nullable @Nullable
public ScreenRect pop() { public ScreenRectangle pop() {
if (stack.isEmpty()) if (stack.isEmpty())
throw new IllegalStateException("Scissor stack underflow"); throw new IllegalStateException("Scissor stack underflow");
stack.removeLast(); stack.removeLast();
@@ -268,48 +293,51 @@ public class RenderScopeImpl implements RenderScope {
} }
@Nullable @Nullable
public ScreenRect peekLast() { public ScreenRectangle peekLast() {
return stack.peekLast(); return stack.peekLast();
} }
public boolean contains(int x, int y) { public boolean contains(int x, int y) {
return stack.isEmpty() || stack.peekLast().contains(x, y); return stack.isEmpty() || stack.peekLast().containsPoint(x, y);
} }
} }
// Item rendering // Item rendering
@Override @Override
public void drawItem(ItemStackRef stack, int x, int y) { public void drawItem(ItemStackRef stack, int x, int y) {
drawItem(null, SaturnClient.client.world, stack, x, y, 0); drawItem(null, SaturnClient.client.level, stack, x, y, 0);
} }
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y, private void drawItem(@Nullable LivingEntity entity, @Nullable Level world, ItemStackRef stack, int x, int y,
int seed) { int seed) {
drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0); drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
} }
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed, private void drawItem(@Nullable LivingEntity entity, @Nullable Level world, ItemStack stack, int x, int y, int seed,
int z) { int z) {
if (!stack.isEmpty()) { if (!stack.isEmpty()) {
//? if >=1.21.7 { //? if >=1.21.7 {
KeyedItemRenderState keyedItemRenderState = new KeyedItemRenderState(); TrackingItemStackRenderState keyedItemRenderState = new TrackingItemStackRenderState();
//?} else //?} else
//ItemRenderState keyedItemRenderState = new ItemRenderState(); //ItemStackRenderState keyedItemRenderState = new ItemStackRenderState();
SaturnClient.client.getItemModelManager().clearAndUpdate(keyedItemRenderState, stack, SaturnClient.client.getItemModelResolver().updateForTopItem(keyedItemRenderState, stack,
ItemDisplayContext.GUI, world, ItemDisplayContext.GUI, world,
entity, seed); entity, seed);
try { try {
this.state.addItem(new ItemGuiElementRenderState(stack.getItem().getName().toString(), //? 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, new Matrix3x2f(this.matrices.stack), keyedItemRenderState, x, y,
this.scissorStack.peekLast())); this.scissorStack.peekLast()));
} catch (Throwable throwable) { } catch (Throwable throwable) {
CrashReport crashReport = CrashReport.create(throwable, "Rendering item"); CrashReport crashReport = CrashReport.forThrowable(throwable, "Rendering item");
CrashReportSection crashReportSection = crashReport.addElement("Item being rendered"); CrashReportCategory crashReportSection = crashReport.addCategory("Item being rendered");
crashReportSection.add("Item Type", () -> String.valueOf(stack.getItem())); crashReportSection.setDetail("Item Type", () -> String.valueOf(stack.getItem()));
crashReportSection.add("Item Components", () -> String.valueOf(stack.getComponents())); crashReportSection.setDetail("Item Components", () -> String.valueOf(stack.getComponents()));
crashReportSection.add("Item Foil", () -> String.valueOf(stack.hasGlint())); crashReportSection.setDetail("Item Foil", () -> String.valueOf(stack.hasFoil()));
throw new CrashException(crashReport); throw new ReportedException(crashReport);
} }
} }
} }
@@ -321,12 +349,12 @@ public class RenderScopeImpl implements RenderScope {
@Override @Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height, int color) { public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height, int color) {
Sprite mcSprite = (Sprite) sprite; TextureAtlasSprite mcSprite = (TextureAtlasSprite) sprite;
if (color == 0 || width == 0 || height == 0) if (color == 0 || width == 0 || height == 0)
return; return;
drawTexturedQuad(mcSprite.getAtlasId(), x, x + width, y, y + height, drawTexturedQuad(mcSprite.atlasLocation(), x, x + width, y, y + height,
mcSprite.getMinU(), mcSprite.getMaxU(), mcSprite.getMinV(), mcSprite.getMaxV(), color); mcSprite.getU0(), mcSprite.getU1(), mcSprite.getV0(), mcSprite.getV1(), color);
} }
@Override @Override
@@ -357,8 +385,458 @@ public class RenderScopeImpl implements RenderScope {
float adjustedScale = scale * scaleX; // apply matrix scale to entity scale too float adjustedScale = scale * scaleX; // apply matrix scale to entity scale too
this.state.addSpecialElement(new EntityGuiElementRenderState(entityState, translation, rotation, this.state.submitPicturesInPictureState(new GuiEntityRenderState(entityState, translation, rotation,
overrideCameraAngle, adjustedX1, adjustedY1, adjustedX2, adjustedY2, adjustedScale, overrideCameraAngle, adjustedX1, adjustedY1, adjustedX2, adjustedY2, adjustedScale,
this.scissorStack.peekLast())); 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_);
}
}
}
*///?}
@@ -3,49 +3,81 @@ package org.saturnclient.impl.ui;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import org.saturnclient.config.Config;
import org.saturnclient.impl.provider.Keys;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen; import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.SaturnScreen.ScreenProvider; import org.saturnclient.ui.SaturnScreen.ScreenProvider;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
//? if >=1.21.9 //? if >=1.21.9
import net.minecraft.client.gui.Click; import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.gui.CubeMapRenderer; import net.minecraft.client.renderer.CubeMap;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.RotatingCubeMapRenderer; //? if >=26.1 {
import net.minecraft.client.gui.screen.Screen; /*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 //? if >=1.21.9
import net.minecraft.client.input.KeyInput; import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.texture.ResourceTexture; //? if <1.21.6 {
import net.minecraft.client.texture.TextureManager; /*import net.minecraft.client.renderer.PostChain;
import net.minecraft.text.Text; import net.minecraft.client.renderer.LevelTargetBundle;
import net.minecraft.util.Identifier; import com.mojang.blaze3d.resource.CrossFrameResourcePool;
*///?}
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
public class SaturnScreenFabric extends Screen implements ScreenProvider { public class SaturnScreenFabric extends Screen implements ScreenProvider {
protected static final Identifier PANORAMA = Identifier.of("saturnclient", protected static final Identifier PANORAMA = Identifier.fromNamespaceAndPath("saturnclient",
"textures/gui/title/background/panorama"); "textures/gui/title/background/panorama");
protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(PANORAMA); public static final CubeMap PANORAMA_RENDERER = new CubeMap(PANORAMA);
public static final RotatingCubeMapRenderer ROTATING_PANORAMA_RENDERER; // 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 { static {
ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER); ROTATING_PANORAMA_RENDERER = new PanoramaRenderer(PANORAMA_RENDERER);
} }
//?}
//? if <1.21.6
//private final CrossFrameResourcePool pool = new CrossFrameResourcePool(3);
public final SaturnScreen screen; public final SaturnScreen screen;
public static void preload(MinecraftClient client) { /** Whether the panorama's textures are loaded; it isn't drawn before (e.g. under the splash). */
TextureManager textureManager = client.getTextureManager(); 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++) { for (int i = 0; i < 6; i++) {
String var10001 = PANORAMA.getPath(); String var10001 = PANORAMA.getPath();
Identifier tex = PANORAMA.withPath(var10001 + "_" + i + ".png"); Identifier tex = PANORAMA.withPath(var10001 + "_" + i + ".png");
ResourceTexture resourceTexture = new ResourceTexture(tex); SimpleTexture resourceTexture = new SimpleTexture(tex);
textureManager.registerTexture(tex, resourceTexture); textureManager.registerAndLoad(tex, resourceTexture);
} }
*///?}
panoramaReady = true;
} }
public SaturnScreenFabric(SaturnScreen screen) { public SaturnScreenFabric(SaturnScreen screen) {
super(Text.literal(screen.title)); super(Component.literal(screen.title));
screen.provider = this; screen.provider = this;
this.screen = screen; this.screen = screen;
} }
@@ -55,30 +87,97 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
screen.init(); screen.init();
} }
//? if <1.21.6
//@SuppressWarnings("deprecation")
@Override @Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) { //? 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) { if (screen.start == null) {
screen.start = Instant.now(); screen.start = Instant.now();
} }
long elapsed = Duration.between(screen.start, Instant.now()).toMillis(); long elapsed = Duration.between(screen.start, Instant.now()).toMillis();
mouseX *= 2; screen.updateScale();
mouseY *= 2;
if (client.world == null && client.getCurrentServerEntry() == null) { mouseX = (int) (mouseX / screen.scale);
// ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, true); 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); RenderScope renderScope = new RenderScopeImpl(context);
screen.render(renderScope, mouseX, mouseY, delta, elapsed); 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. // 1.21.9 moved mouse and key events to Click and KeyInput.
//? if >=1.21.9 { //? if >=1.21.9 {
@Override @Override
public boolean mouseClicked(Click click, boolean doubled) { public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
if (screen.mouseClicked(click.x(), click.y(), click.button())) { if (screen.mouseClicked(click.x(), click.y(), Keys.mouseButtonToSaturn(click.button()))) {
return true; return true;
} }
@@ -86,9 +185,9 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
} }
@Override @Override
public boolean mouseDragged(Click click, double offsetX, double offsetY) { public boolean mouseDragged(MouseButtonEvent click, double offsetX, double offsetY) {
// TODO: 1.21.8 passes the drag deltas; check whether offsetX/offsetY are the same on 1.21.9+. // offsetX and offsetY are the drag deltas, as mouseDragged's deltaX and deltaY were before 1.21.9.
if (screen.mouseDragged(click.x(), click.y(), click.button(), 0.0, 0.0)) { if (screen.mouseDragged(click.x(), click.y(), Keys.mouseButtonToSaturn(click.button()), offsetX, offsetY)) {
return true; return true;
} }
@@ -96,8 +195,8 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
} }
@Override @Override
public boolean mouseReleased(Click click) { public boolean mouseReleased(MouseButtonEvent click) {
if (screen.mouseReleased(click.x(), click.y(), click.button())) { if (screen.mouseReleased(click.x(), click.y(), Keys.mouseButtonToSaturn(click.button()))) {
return true; return true;
} }
@@ -139,8 +238,12 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
//? if >=1.21.9 { //? if >=1.21.9 {
@Override @Override
public boolean keyPressed(KeyInput input) { public boolean keyPressed(KeyEvent input) {
if (screen.keyPressed(input.getKeycode(), input.scancode(), input.modifiers())) { // 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 true;
} }
@@ -156,28 +259,39 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
} }
*///?} *///?}
// TODO: resize isn't hooked on 1.21.11 yet. //? if >=1.21.11 {
//? if <1.21.11 { @Override
public void resize(int width, int height) {
screen.resize(width, height);
super.resize(width, height);
}
//?} else {
/*@Override /*@Override
public void resize(MinecraftClient client, int width, int height) { public void resize(Minecraft client, int width, int height) {
screen.resize(width, height); screen.resize(width, height);
super.resize(client, width, height); super.resize(client, width, height);
} }
*///?} *///?}
@Override @Override
public void close() { public void onClose() {
this.screen.onClose(); this.screen.onClose();
super.close(); super.onClose();
}
// ScreenProvider's close, separate from the vanilla onClose above.
@Override
public void close() {
onClose();
} }
@Override @Override
public int getWidth() { public int getWidth() {
return width * 2; return width;
} }
@Override @Override
public int getHeight() { public int getHeight() {
return height * 2; 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);
}
}
@@ -3,7 +3,7 @@ package org.saturnclient.saturnclient;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import org.saturnclient.client.ServiceClient; import org.saturnclient.client.ServiceClient;
import org.saturnclient.cosmetics.Emotes; import org.saturnclient.cosmetics.Emotes;
@@ -29,12 +29,12 @@ public class SaturnClient implements ModInitializer {
public static final String MOD_ID = "saturnclient"; public static final String MOD_ID = "saturnclient";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static MinecraftClient client; public static Minecraft client;
@Override @Override
public void onInitialize() { public void onInitialize() {
LOGGER.info("Initializing " + MOD_ID); LOGGER.info("Initializing " + MOD_ID);
client = MinecraftClient.getInstance(); client = Minecraft.getInstance();
Providers.saturn = new SaturnProviderImpl(); Providers.saturn = new SaturnProviderImpl();
Providers.refConstructor = new RefConstructorImpl(); Providers.refConstructor = new RefConstructorImpl();
@@ -1,21 +1,25 @@
package org.saturnclient.saturnclient.event; package org.saturnclient.saturnclient.event;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
//? if >=26.1 {
/*import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
*///?} else
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.option.KeyBinding; import net.minecraft.client.KeyMapping;
import net.minecraft.client.util.InputUtil; import com.mojang.blaze3d.platform.InputConstants;
//? if >=1.21.9 //? if >=1.21.9
import net.minecraft.util.Identifier; import net.minecraft.resources.Identifier;
import org.saturnclient.config.Config; import org.saturnclient.config.Config;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.common.provider.Providers; import org.saturnclient.common.provider.Providers;
import org.saturnclient.client.ServiceClient; import org.saturnclient.client.ServiceClient;
import org.saturnclient.emotes.fabric.Emotes; import org.saturnclient.emotes.fabric.Emotes;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.impl.ui.Screens;
import org.saturnclient.ui.screens.EmoteWheel; import org.saturnclient.ui.screens.EmoteWheel;
import org.saturnclient.ui.screens.ShiftMenu; import org.saturnclient.ui.screens.ShiftMenu;
import org.lwjgl.glfw.GLFW;
/** /**
* Handles keyboard input events for the Saturn Client. * Handles keyboard input events for the Saturn Client.
@@ -23,46 +27,62 @@ import org.lwjgl.glfw.GLFW;
*/ */
public class KeyInputHandler { public class KeyInputHandler {
//? if >=1.21.9 { //? if >=1.21.9 {
private static final Identifier KEY_CATEGORY = Identifier.of("saturnclient", "key.category.saturnclient"); private static final Identifier KEY_CATEGORY = Identifier.fromNamespaceAndPath("saturnclient", "key.category.saturnclient");
//?} else //?} else
//private static final String KEY_CATEGORY = "key.category.saturnclient"; //private static final String KEY_CATEGORY = "key.category.saturnclient";
private static final String MAIN_MENU_KEY_ID = "key.saturnclient.main_menu"; private static final String MAIN_MENU_KEY_ID = "key.saturnclient.main_menu";
private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT; private static final int DEFAULT_MENU_KEY = InputConstants.KEY_RSHIFT;
private static KeyBinding mainMenuKeyBinding; private static KeyMapping mainMenuKeyBinding;
public static void register() { public static void register() {
//? if >=1.21.9 { //? if >=26.1 {
mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding( /*mainMenuKeyBinding = KeyMappingHelper.registerKeyMapping(
new KeyBinding( new KeyMapping(
MAIN_MENU_KEY_ID, MAIN_MENU_KEY_ID,
DEFAULT_MENU_KEY, DEFAULT_MENU_KEY,
KeyBinding.Category.create(KEY_CATEGORY))); KeyMapping.Category.register(KEY_CATEGORY)));
//?} else { *///?} else if >=1.21.9 {
/*mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding( mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(
new KeyMapping(
MAIN_MENU_KEY_ID, MAIN_MENU_KEY_ID,
InputUtil.Type.KEYSYM, DEFAULT_MENU_KEY,
KeyMapping.Category.register(KEY_CATEGORY)));
//?} else {
/*mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyMapping(
MAIN_MENU_KEY_ID,
InputConstants.Type.KEYSYM,
DEFAULT_MENU_KEY, DEFAULT_MENU_KEY,
KEY_CATEGORY)); KEY_CATEGORY));
*///?} *///?}
// Register the event handler for the main menu key // Register the event handler for the main menu key
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (mainMenuKeyBinding.wasPressed()) { if (mainMenuKeyBinding.consumeClick()) {
Providers.saturn.getClient().setScreen(new ShiftMenu()); Providers.saturn.getClient().setScreen(new ShiftMenu());
} }
if (Config.openEmoteWheel.wasKeyPressed() && client.currentScreen == null // The emote wheel key opens the wheel, and closes it again while it's open. Closing it
//? if >=1.21.9 { // from the screen's own key handling would reopen it on the next tick, since the key is
&& !InputUtil.isKeyPressed(client.getWindow(), GLFW.GLFW_KEY_F3)) { // still down and no screen is open by then.
if (Config.openEmoteWheel.wasKeyPressed()
//? if >=26.3 {
/*&& !InputConstants.isKeyDown(InputConstants.KEY_F3)) {
*///?} else if >=1.21.9 {
&& !InputConstants.isKeyDown(client.getWindow(), InputConstants.KEY_F3)) {
//?} else //?} else
//&& !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) { //&& !InputConstants.isKeyDown(client.getWindow().getWindow(), InputConstants.KEY_F3)) {
if (Screens.current() == null) {
Providers.saturn.getClient().setScreen(new EmoteWheel()); Providers.saturn.getClient().setScreen(new EmoteWheel());
} else if (Screens.current() instanceof SaturnScreenFabric saturnScreen
&& saturnScreen.screen instanceof EmoteWheel) {
saturnScreen.onClose();
}
} }
// Sneaking cancels your own emote, for everyone. // Sneaking cancels your own emote, for everyone.
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking() if (SaturnClient.client.player != null && SaturnClient.client.player.isShiftKeyDown()
&& Emotes.playing(SaturnClient.client.player.getUuid()).isPresent()) { && Emotes.playing(SaturnClient.client.player.getUUID()).isPresent()) {
Emotes.stop(SaturnClient.client.player.getUuid()); Emotes.stop(SaturnClient.client.player.getUUID());
ServiceClient.emote(""); ServiceClient.emote("");
} }
}); });
@@ -1,12 +1,20 @@
package org.saturnclient.saturnclient.mixin; package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.GuiGraphics;
//? if >=1.21.6 {
import net.minecraft.client.gui.render.state.GuiRenderState; import net.minecraft.client.gui.render.state.GuiRenderState;
//?} else
//import net.minecraft.client.renderer.MultiBufferSource;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DrawContext.class) @Mixin(GuiGraphics.class)
public interface DrawContextAccessor { public interface DrawContextAccessor {
@Accessor("state") //? if >=1.21.6 {
@Accessor("guiRenderState")
GuiRenderState getState(); GuiRenderState getState();
//?} else {
/*@Accessor("bufferSource")
MultiBufferSource.BufferSource getVertexConsumers();
*///?}
} }
@@ -0,0 +1,46 @@
package org.saturnclient.saturnclient.mixin;
//? if >=26.1 {
/*import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.gui.render.GuiRenderer;
import net.minecraft.client.renderer.CubeMap;
import org.saturnclient.impl.ui.GlassRenderer;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.impl.ui.Screens;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
*///?} else
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
/**
* From 26.1 GuiRenderer draws the menu panorama from its own cube map, which is vanilla's. While a
* Saturn screen is open, this draws Saturn's panorama instead. It also blurs the glass backdrops
* once the panorama is drawn. Before 26.1 SaturnScreenFabric draws
* the panorama itself, so this mixin is empty there, and it targets Minecraft because GuiRenderer
* doesn't exist before 1.21.6. It stays registered, keeping saturnclient.mixins.json the same for
* every version.
*/
//? if >=26.1 {
/*@Mixin(GuiRenderer.class)
public class GuiRendererMixin {
@WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/CubeMap;render(FF)V"))
private void saturn$drawSaturnPanorama(CubeMap cubeMap, float xRot, float yRot, Operation<Void> original) {
boolean saturnScreen = Screens.current() instanceof SaturnScreenFabric;
original.call(saturnScreen ? SaturnScreenFabric.PANORAMA_RENDERER : cubeMap, xRot, yRot);
}
// The glass backdrops are copied here, with the frame's world and panorama drawn and the GUI not
// yet: the GUI is built before the frame is drawn (see GlassRenderer).
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/GuiRenderer;prepare()V"))
private void saturn$blurGlassBackdrops(CallbackInfo ci) {
GlassRenderer.blurPendingBackdrops();
}
}
*///?} else {
@Mixin(Minecraft.class)
public class GuiRendererMixin {
}
//?}
@@ -1,20 +1,23 @@
package org.saturnclient.saturnclient.mixin; package org.saturnclient.saturnclient.mixin;
//? if >=1.21.9 { //? if >=1.21.9 {
import net.minecraft.client.network.ClientPlayerLikeEntity; import net.minecraft.client.entity.ClientAvatarEntity;
import net.minecraft.entity.PlayerLikeEntity; import net.minecraft.world.entity.Avatar;
//?} else //?} else
//import net.minecraft.client.network.AbstractClientPlayerEntity; //import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.render.entity.EntityRendererFactory; import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.render.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.PlayerEntityRenderer; import net.minecraft.client.renderer.entity.player.AvatarRenderer;
import net.minecraft.client.render.entity.model.PlayerEntityModel; //? if >=1.21.11 {
import net.minecraft.client.render.entity.state.PlayerEntityRenderState; import net.minecraft.client.model.player.PlayerModel;
import net.minecraft.entity.Entity; //?} else
import net.minecraft.entity.player.PlayerEntity; //import net.minecraft.client.model.PlayerModel;
import net.minecraft.text.MutableText; import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import net.minecraft.text.Text; import net.minecraft.world.entity.Entity;
import net.minecraft.util.Formatting; import net.minecraft.world.entity.player.Player;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Component;
import net.minecraft.ChatFormatting;
import org.saturnclient.client.player.Roles; import org.saturnclient.client.player.Roles;
import org.saturnclient.client.player.SaturnPlayer; import org.saturnclient.client.player.SaturnPlayer;
@@ -26,56 +29,54 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PlayerEntityRenderer.class) @Mixin(AvatarRenderer.class)
public abstract class PlayerEntityRendererMixin public abstract class PlayerEntityRendererMixin
extends LivingEntityRenderer<PlayerEntity, PlayerEntityRenderState, PlayerEntityModel> { extends LivingEntityRenderer<Player, AvatarRenderState, PlayerModel> {
protected PlayerEntityRendererMixin() { protected PlayerEntityRendererMixin() {
super(null, null, 0.0f); super(null, null, 0.0f);
} }
@Inject(method = "<init>", at = @At("TAIL")) @Inject(method = "<init>", at = @At("TAIL"))
private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) { private void addCustomFeatureRenderer(EntityRendererProvider.Context ctx, boolean slim, CallbackInfo ci) {
this.addFeature(new HatFeatureRenderer(this)); this.addLayer(new HatFeatureRenderer(this));
this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader())); this.addLayer(new CloakFeatureRenderer(this, ctx.getEquipmentAssets()));
} }
// Stores the SaturnPlayer in the render state and adds the role icon to the name. Render state // Adds the role icon to the player's name, and stores the SaturnPlayer in the render state for the
// data only exists from 1.21.8. // cloak and hat (Fabric API only has render state data from 1.21.8).
// TODO: 1.21.6 and 1.21.7 don't show the role icon in player names.
//? if >=1.21.9 { //? if >=1.21.9 {
@Inject(method = "updateRenderState", at = @At("TAIL")) @Inject(method = "extractRenderState", at = @At("TAIL"))
public <A extends PlayerLikeEntity & ClientPlayerLikeEntity> void updateRenderState( public <A extends Avatar & ClientAvatarEntity> void updateRenderState(
A entity, PlayerEntityRenderState state, float f, CallbackInfo ci) { A entity, AvatarRenderState state, float f, CallbackInfo ci) {
saturn$applyPlayer(entity, state); saturn$applyPlayer(entity, state);
} }
//?} else if >=1.21.8 { //?} else {
/*@Inject(method = "updateRenderState", at = @At("TAIL")) /*@Inject(method = "extractRenderState", at = @At("TAIL"))
public void updateRenderState( public void updateRenderState(
AbstractClientPlayerEntity entity, PlayerEntityRenderState state, float f, CallbackInfo ci) { AbstractClientPlayer entity, AvatarRenderState state, float f, CallbackInfo ci) {
saturn$applyPlayer(entity, state); saturn$applyPlayer(entity, state);
} }
*///?} *///?}
//? if >=1.21.8 { private void saturn$applyPlayer(Entity entity, AvatarRenderState state) {
private void saturn$applyPlayer(Entity entity, PlayerEntityRenderState state) {
if (entity == null) { if (entity == null) {
return; return;
} }
SaturnPlayer player = SaturnPlayer.get(entity.getUuid()); SaturnPlayer player = SaturnPlayer.get(entity.getUUID());
//? if >=1.21.8
state.setData(SaturnRenderState.saturnDataKey, player); state.setData(SaturnRenderState.saturnDataKey, player);
if (player != null && state.displayName != null) { if (player != null && state.nameTag != null) {
MutableText iconText = Text.literal(Roles.getSaturnIndicator()) MutableComponent iconText = Component.literal(Roles.getSaturnIndicator())
.styled(style -> style.withColor(Roles.getIconColor(player.uuid))); .withStyle(style -> style.withColor(Roles.getIconColor(player.uuid)));
state.displayName = Text.empty() state.nameTag = Component.empty()
.append(iconText.copy()) .append(iconText.copy())
.append(Text.empty().formatted(Formatting.RESET)) .append(Component.empty().withStyle(ChatFormatting.RESET))
.append(state.displayName.copy()); .append(state.nameTag.copy());
} }
} }
//?}
} }
@@ -7,18 +7,24 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.ModifyVariable;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screen.Screen; //? if >=26.2
import net.minecraft.client.gui.screen.TitleScreen; //import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.TitleScreen;
@Mixin(MinecraftClient.class) // Screens are opened through Gui from 26.2 (through Minecraft before).
//? if >=26.2 {
/*@Mixin(Gui.class)
*///?} else
@Mixin(Minecraft.class)
public class SetScreenMixin { public class SetScreenMixin {
@ModifyVariable(method = "setScreen", at = @At("HEAD"), argsOnly = true) @ModifyVariable(method = "setScreen", at = @At("HEAD"), argsOnly = true)
private Screen replaceTitleScreen(Screen screen) { private Screen replaceTitleScreen(Screen screen) {
if (Config.saturnTitleScreen.value) { if (Config.saturnTitleScreen.value) {
if (screen instanceof TitleScreen) if (screen instanceof TitleScreen)
return new SaturnScreenFabric(new TitleMenu()); return new SaturnScreenFabric(new TitleMenu());
if (screen == null && ((MinecraftClient) (Object) this).world == null) if (screen == null && Minecraft.getInstance().level == null)
return new SaturnScreenFabric(new TitleMenu()); return new SaturnScreenFabric(new TitleMenu());
} }
@@ -1,5 +1,7 @@
package org.saturnclient.saturnclient.mixin; package org.saturnclient.saturnclient.mixin;
import org.saturnclient.impl.ui.Screens;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -12,67 +14,73 @@ import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screen.SplashOverlay; import net.minecraft.client.gui.screens.LoadingOverlay;
//? if >=1.21.11 //? if >=1.21.11
import net.minecraft.client.util.Window; import com.mojang.blaze3d.platform.Window;
import net.minecraft.resource.ResourceReload; import net.minecraft.server.packs.resources.ReloadInstance;
//? if >=1.21.11 {
import net.minecraft.util.Util; import net.minecraft.util.Util;
import net.minecraft.util.math.MathHelper; //?} else
//import net.minecraft.Util;
import net.minecraft.util.Mth;
@Mixin(SplashOverlay.class) @Mixin(LoadingOverlay.class)
public abstract class SplashOverlayMixin { public abstract class SplashOverlayMixin {
@Shadow @Shadow
private MinecraftClient client; private Minecraft minecraft;
@Shadow @Shadow
private ResourceReload reload; private ReloadInstance reload;
@Shadow @Shadow
private long reloadCompleteTime; private long fadeOutStart;
@Shadow @Shadow
private long reloadStartTime; private long fadeInStart;
@Shadow @Shadow
@Final @Final
private boolean reloading; private boolean fadeIn;
@Shadow @Shadow
@Final @Final
private Consumer<Optional<Throwable>> exceptionHandler; private Consumer<Optional<Throwable>> onFinish;
@Shadow @Shadow
private float progress; private float currentProgress;
/** /**
* @author SaturnClient * @author SaturnClient
* @reason Replace vanilla splash rendering completely but keep exit logic * @reason Replace vanilla splash rendering completely but keep exit logic
*/ */
@Overwrite @Overwrite
public void render(DrawContext context, int mouseX, int mouseY, float delta) { //? if >=26.1 {
int screenWidth = context.getScaledWindowWidth(); /*public void extractRenderState(GuiGraphics context, int mouseX, int mouseY, float delta) {
int screenHeight = context.getScaledWindowHeight(); *///?} else
long currentTime = Util.getMeasuringTimeMs(); public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {
int screenWidth = context.guiWidth();
int screenHeight = context.guiHeight();
long currentTime = Util.getMillis();
// Initialize reload start time if not set // Initialize reload start time if not set
if (reloadStartTime == -1L) { if (fadeInStart == -1L) {
reloadStartTime = currentTime; fadeInStart = currentTime;
} }
// Compute fade-out progress after reload completes // Compute fade-out progress after reload completes
float fadeOutProgress = reloadCompleteTime > -1L float fadeOutProgress = fadeOutStart > -1L
? (float) (currentTime - reloadCompleteTime) / 1000f ? (float) (currentTime - fadeOutStart) / 1000f
: -1f; : -1f;
int alpha = fadeOutProgress >= 0f int alpha = fadeOutProgress >= 0f
? MathHelper.ceil((1.0f - MathHelper.clamp(fadeOutProgress - 1f, 0f, 1f)) * 255f) ? Mth.ceil((1.0f - Mth.clamp(fadeOutProgress - 1f, 0f, 1f)) * 255f)
: 255; : 255;
// Compute fade-in progress while reloading // Compute fade-in progress while reloading
float reloadProgress = reloadStartTime > -1L float reloadProgress = fadeInStart > -1L
? (float) (currentTime - reloadStartTime) / 500f ? (float) (currentTime - fadeInStart) / 500f
: -1f; : -1f;
// Get render scope for better UI // Get render scope for better UI
@@ -84,7 +92,7 @@ public abstract class SplashOverlayMixin {
int overlayAlpha = computeOverlayAlpha(fadeOutProgress - 1f); int overlayAlpha = computeOverlayAlpha(fadeOutProgress - 1f);
scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight, scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
Theme.withAlpha(overlayAlpha, 0xFFFFFFFF)); Theme.withAlpha(overlayAlpha, 0xFFFFFFFF));
} else if (reloading) { } else if (fadeIn) {
if (reloadProgress < 1.0F) { if (reloadProgress < 1.0F) {
renderCurrentScreen(context, mouseX, mouseY, delta); renderCurrentScreen(context, mouseX, mouseY, delta);
} }
@@ -95,8 +103,8 @@ public abstract class SplashOverlayMixin {
Theme.withAlpha(alpha, 0xFFFFFFFF)); Theme.withAlpha(alpha, 0xFFFFFFFF));
} }
float u = this.reload.getProgress(); float u = this.reload.getActualProgress();
this.progress = MathHelper.clamp(this.progress * 0.95F + u * 0.050000012F, 0.0F, 1.0F); this.currentProgress = Mth.clamp(this.currentProgress * 0.95F + u * 0.050000012F, 0.0F, 1.0F);
// Draw central logo // Draw central logo
drawLogo(scope, screenWidth, screenHeight, alpha); drawLogo(scope, screenWidth, screenHeight, alpha);
@@ -104,16 +112,19 @@ public abstract class SplashOverlayMixin {
// Remove overlay after fade-out completes // Remove overlay after fade-out completes
if (fadeOutProgress >= 2.0F) { if (fadeOutProgress >= 2.0F) {
client.setOverlay(null); Screens.clearOverlay();
} }
// Handle reload completion and exceptions // Handle reload completion and exceptions
handleReloadCompletion(context); handleReloadCompletion(context);
} }
private void renderCurrentScreen(DrawContext context, int mouseX, int mouseY, float delta) { private void renderCurrentScreen(GuiGraphics context, int mouseX, int mouseY, float delta) {
if (client.currentScreen != null) { if (Screens.current() != null) {
client.currentScreen.render(context, mouseX, mouseY, delta); //? if >=26.1 {
/*Screens.current().extractRenderState(context, mouseX, mouseY, delta);
*///?} else
Screens.current().render(context, mouseX, mouseY, delta);
} }
} }
@@ -123,14 +134,19 @@ public abstract class SplashOverlayMixin {
Theme.withAlpha(alpha, 0xFFFFFFFF)); Theme.withAlpha(alpha, 0xFFFFFFFF));
} }
private void renderProgressBar(RenderScope scope, DrawContext context, int screenWidth, int screenHeight, private void renderProgressBar(RenderScope scope, GuiGraphics context, int screenWidth, int screenHeight,
int alpha) { int alpha) {
int progressY = ((screenHeight - 5) / 2) + 25; int progressY = ((screenHeight - 5) / 2) + 25;
// TODO: the loading bar's border isn't drawn on 1.21.9+ yet. //? if >=26.1 {
//? if <1.21.9 /*context.outline((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
//context.drawBorder((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF)); *///?} else if >=1.21.11 {
context.renderOutline((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
//?} else if >=1.21.9 {
/*context.submitOutline((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
*///?} else
//context.renderOutline((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
int progressWidth = Math.min((int) (progress * 120), 120); int progressWidth = Math.min((int) (currentProgress * 120), 120);
int progressX = (screenWidth - 120) / 2; int progressX = (screenWidth - 120) / 2;
context.fill(progressX, progressY, progressX + progressWidth, progressY + 5, context.fill(progressX, progressY, progressX + progressWidth, progressY + 5,
@@ -139,31 +155,31 @@ public abstract class SplashOverlayMixin {
/** Compute overlay alpha from fade progress */ /** Compute overlay alpha from fade progress */
private int computeOverlayAlpha(float progress) { private int computeOverlayAlpha(float progress) {
return MathHelper.ceil((1.0F - MathHelper.clamp(progress, 0.0F, 1.0F)) * 255.0F); return Mth.ceil((1.0F - Mth.clamp(progress, 0.0F, 1.0F)) * 255.0F);
} }
/** Handle reload completion logic */ /** Handle reload completion logic */
private void handleReloadCompletion(DrawContext context) { private void handleReloadCompletion(GuiGraphics context) {
float reloadProgress = reloadStartTime > -1L ? (float) (Util.getMeasuringTimeMs() - reloadStartTime) / 500f float reloadProgress = fadeInStart > -1L ? (float) (Util.getMillis() - fadeInStart) / 500f
: -1f; : -1f;
if (reloadCompleteTime == -1L && reload.isComplete() && (!reloading || reloadProgress >= 2.0F)) { if (fadeOutStart == -1L && reload.isDone() && (!fadeIn || reloadProgress >= 2.0F)) {
try { try {
reload.throwException(); reload.checkExceptions();
exceptionHandler.accept(Optional.empty()); onFinish.accept(Optional.empty());
} catch (Throwable ex) { } catch (Throwable ex) {
exceptionHandler.accept(Optional.of(ex)); onFinish.accept(Optional.of(ex));
} }
reloadCompleteTime = Util.getMeasuringTimeMs(); fadeOutStart = Util.getMillis();
//? if >=1.21.11 //? if >=1.21.11
Window window = client.getWindow(); Window window = minecraft.getWindow();
if (client.currentScreen != null) { if (Screens.current() != null) {
//? if >=1.21.11 { //? if >=1.21.11 {
client.currentScreen.init(window.getScaledWidth(), window.getScaledHeight()); Screens.current().init(window.getGuiScaledWidth(), window.getGuiScaledHeight());
//?} else //?} else
//client.currentScreen.init(client, context.getScaledWindowWidth(), context.getScaledWindowHeight()); //Screens.current().init(minecraft, context.guiWidth(), context.guiHeight());
} }
} }
} }
@@ -1,8 +1,8 @@
package org.saturnclient.saturnclient.mixin; package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.network.PlayerListEntry; import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.scoreboard.Team; import net.minecraft.world.scores.PlayerTeam;
import net.minecraft.text.Text; import net.minecraft.network.chat.Component;
import java.util.UUID; import java.util.UUID;
@@ -15,7 +15,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
@Mixin(PlayerListEntry.class) @Mixin(PlayerInfo.class)
public abstract class TabListMixin { public abstract class TabListMixin {
/** /**
@@ -23,9 +23,9 @@ public abstract class TabListMixin {
* Saturn Client, keeping any name, rank prefix or team formatting the server * Saturn Client, keeping any name, rank prefix or team formatting the server
* set. * set.
*/ */
@Inject(method = "getDisplayName", at = @At("RETURN"), cancellable = true) @Inject(method = "getTabListDisplayName", at = @At("RETURN"), cancellable = true)
private void saturn$addSaturnIcon(CallbackInfoReturnable<Text> cir) { private void saturn$addSaturnIcon(CallbackInfoReturnable<Component> cir) {
PlayerListEntry entry = (PlayerListEntry) (Object) this; PlayerInfo entry = (PlayerInfo) (Object) this;
GameProfile profile = entry.getProfile(); GameProfile profile = entry.getProfile();
//? if >=1.21.9 { //? if >=1.21.9 {
String name = profile.name(); String name = profile.name();
@@ -39,16 +39,16 @@ public abstract class TabListMixin {
return; return;
} }
Text displayName = cir.getReturnValue(); Component displayName = cir.getReturnValue();
// With no server-set name, vanilla shows the team-decorated profile name. // With no server-set name, vanilla shows the team-decorated profile name.
if (displayName == null) { if (displayName == null) {
displayName = Team.decorateName(entry.getScoreboardTeam(), Text.literal(name)); displayName = PlayerTeam.formatNameForTeam(entry.getTeam(), Component.literal(name));
} }
cir.setReturnValue(Text.empty() cir.setReturnValue(Component.empty()
.append(Text.literal(Roles.getSaturnIndicator()) .append(Component.literal(Roles.getSaturnIndicator())
.styled(style -> style.withColor(Roles.getIconColor(uuid)))) .withStyle(style -> style.withColor(Roles.getIconColor(uuid))))
.append(displayName)); .append(displayName));
} }
} }
@@ -12,14 +12,14 @@
package org.saturnclient.saturnclient.mixin; package org.saturnclient.saturnclient.mixin;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.entity.Entity; import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@Mixin(LivingEntityRenderer.class) @Mixin(LivingEntityRenderer.class)
public class WhoAmI { public class WhoAmI {
@ModifyExpressionValue(method = "hasLabel(Lnet/minecraft/entity/LivingEntity;D)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getCameraEntity()Lnet/minecraft/entity/Entity;")) @ModifyExpressionValue(method = "shouldShowName(Lnet/minecraft/world/entity/LivingEntity;D)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getCameraEntity()Lnet/minecraft/world/entity/Entity;"))
public Entity obfuscateCameraEntity(Entity original) { public Entity obfuscateCameraEntity(Entity original) {
// hasLabel checks if the rendered entity is equal to the camera entity (aka our // hasLabel checks if the rendered entity is equal to the camera entity (aka our
// player), // player),
@@ -0,0 +1,55 @@
#version 330
// 26.3 compiles shaders through SPIR-V, which needs every input and output's location.
#ifdef EXPLICIT_LOCATIONS
#extension GL_ARB_separate_shader_objects : require
#endif
// Saturn's glass: the blurred game behind the screen, cut to a panel's shape. For 1.21.6 and
// later; glass_pre_1_21_6.fsh (with glass.json, the program 1.21.4 loads) is for older versions.
// Vanilla's transforms, laid out as the version has them: 1.21.6 to 1.21.10 end with LineWidth, which
// 1.21.11 dropped (GlassRenderer defines HAS_LINE_WIDTH there), and 26.3 moved TextureMat up
// (TEXTURE_MAT_SECOND). Not imported: the include only exists from 1.21.6, and 1.21.4 and 1.21.5 still
// read every shader's imports while loading resources.
layout(std140) uniform DynamicTransforms {
mat4 ModelViewMat;
#ifdef TEXTURE_MAT_SECOND
mat4 TextureMat;
#endif
vec4 ColorModulator;
vec3 ModelOffset;
#ifndef TEXTURE_MAT_SECOND
mat4 TextureMat;
#endif
#ifdef HAS_LINE_WIDTH
float LineWidth;
#endif
};
// The panel's rounded rectangle, the same texture its background is drawn with
uniform sampler2D Sampler0;
// The game behind the screen, blurred, the size of the framebuffer
uniform sampler2D Sampler1;
#ifdef EXPLICIT_LOCATIONS
layout(location = 0) in vec2 texCoord0;
layout(location = 1) in vec4 vertexColor;
layout(location = 0) out vec4 fragColor;
#else
in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;
#endif
void main() {
float mask = texture(Sampler0, texCoord0).a * vertexColor.a;
if (mask == 0.0) {
discard;
}
// The blurred copy lines up with the framebuffer being drawn to.
vec2 screenUv = gl_FragCoord.xy / vec2(textureSize(Sampler1, 0));
fragColor = vec4(texture(Sampler1, screenUv).rgb, mask) * ColorModulator;
}
@@ -0,0 +1,13 @@
{
"vertex": "minecraft:core/position_tex_color",
"fragment": "saturnclient:core/glass_pre_1_21_6",
"samplers": [
{ "name": "Sampler0" },
{ "name": "Sampler1" }
],
"uniforms": [
{ "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }
]
}
@@ -0,0 +1,27 @@
#version 150
// Saturn's glass (see glass.fsh), for 1.21.4 and 1.21.5, which set uniforms one by one instead of in
// blocks.
uniform vec4 ColorModulator;
// The panel's rounded rectangle, the same texture its background is drawn with
uniform sampler2D Sampler0;
// The game behind the screen, blurred, the size of the framebuffer
uniform sampler2D Sampler1;
in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;
void main() {
float mask = texture(Sampler0, texCoord0).a * vertexColor.a;
if (mask == 0.0) {
discard;
}
// The blurred copy lines up with the framebuffer being drawn to.
vec2 screenUv = gl_FragCoord.xy / vec2(textureSize(Sampler1, 0));
fragColor = vec4(texture(Sampler1, screenUv).rgb, mask) * ColorModulator;
}
+1 -1
View File
@@ -22,7 +22,7 @@
], ],
"depends": { "depends": {
"fabricloader": ">=${loader_version}", "fabricloader": ">=${loader_version}",
"fabric": "*", "fabric-api": "*",
"minecraft": "${minecraft_version}" "minecraft": "${minecraft_version}"
} }
} }
+1 -2
View File
@@ -2,14 +2,13 @@
"required": true, "required": true,
"minVersion": "0.8", "minVersion": "0.8",
"package": "org.saturnclient.impl.features.mixins", "package": "org.saturnclient.impl.features.mixins",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "${java}",
"client": [ "client": [
"render.GameRendererMixin", "render.GameRendererMixin",
"render.LightmapTextureManagerMixin", "render.LightmapTextureManagerMixin",
"render.LivingEntityRendererMixin", "render.LivingEntityRendererMixin",
"render.LivingEntityRenderStateMixin", "render.LivingEntityRenderStateMixin",
"render.BackgroundRendererMixin", "render.BackgroundRendererMixin",
"misc.NametagsMixin",
"render.RenderMixin", "render.RenderMixin",
"world.TpsMixin", "world.TpsMixin",
"entity.CameraMixin", "entity.CameraMixin",
+2 -1
View File
@@ -2,10 +2,11 @@
"required": true, "required": true,
"minVersion": "0.8", "minVersion": "0.8",
"package": "org.saturnclient.impl.mixins", "package": "org.saturnclient.impl.mixins",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "${java}",
"client": [ "client": [
"IdentifierMixin", "IdentifierMixin",
"ItemStackMixin", "ItemStackMixin",
"MatrixStackMixin",
"MinecraftClientMixin", "MinecraftClientMixin",
"QuaternionfMixin", "QuaternionfMixin",
"WindowMixin", "WindowMixin",
+3 -2
View File
@@ -2,14 +2,15 @@
"required": true, "required": true,
"minVersion": "0.8", "minVersion": "0.8",
"package": "org.saturnclient.saturnclient.mixin", "package": "org.saturnclient.saturnclient.mixin",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "${java}",
"client": [ "client": [
"TabListMixin", "TabListMixin",
"PlayerEntityRendererMixin", "PlayerEntityRendererMixin",
"WhoAmI", "WhoAmI",
"DrawContextAccessor", "DrawContextAccessor",
"SplashOverlayMixin", "SplashOverlayMixin",
"SetScreenMixin" "SetScreenMixin",
"GuiRendererMixin"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1
+35 -5
View File
@@ -6,6 +6,31 @@ plugins {
// "Reset active project" before committing. // "Reset active project" before committing.
stonecutter active "1.21.11" stonecutter active "1.21.11"
// src/ uses 1.21.11's Mojang names (the active version). These names changed in the versions given,
// so Stonecutter swaps in each version's name when it processes it.
stonecutter parameters {
for ((since, old, new) in listOf(
Triple("1.21.11", "ResourceLocation", "Identifier"),
Triple("1.21.9", "PlayerRenderer", "AvatarRenderer"),
Triple("1.21.9", "PlayerRenderState", "AvatarRenderState"),
Triple("26.1", "GuiGraphics", "GuiGraphicsExtractor"),
Triple("26.1", "net.minecraft.client.gui.render.state", "net.minecraft.client.renderer.state.gui"),
// GuiRenderState's methods
Triple("26.1", "submitGuiElement", "addGuiElement"),
Triple("26.1", "submitText", "addText"),
Triple("26.1", "submitItem", "addItem"),
Triple("26.1", "submitPicturesInPictureState", "addPicturesInPictureState"),
// 26.3 moves Blaze3D's API to renderpearl
Triple("26.3", "com.mojang.blaze3d.GpuFormat", "com.mojang.renderpearl.api.GpuFormat"),
Triple("26.3", "com.mojang.blaze3d.pipeline.RenderPipeline", "com.mojang.renderpearl.api.pipeline.RenderPipeline"),
Triple("26.3", "com.mojang.blaze3d.textures", "com.mojang.renderpearl.api.textures"),
)) {
replacements.regex(eval(current.version, ">=$since")) {
replace("\\b${Regex.escape(old)}\\b", new, "\\b${Regex.escape(new)}\\b", old)
}
}
}
subprojects { subprojects {
apply(plugin = "java") apply(plugin = "java")
@@ -55,17 +80,22 @@ subprojects {
tasks.register("buildAll") { tasks.register("buildAll") {
group = "build" group = "build"
description = "Builds every Minecraft version's remapped jar and collects them in build/allJars" description = "Builds every Minecraft version's mod jar and collects them in build/allJars"
val remapJars = subprojects.flatMap { it.tasks.matching { task -> task.name == "remapJar" } } // The mod jar is remapJar's output for 1.21.x, and jar's for 26.1+, which isn't obfuscated.
dependsOn(remapJars) val modJars = subprojects.filter { it.name != "common" }.flatMap { project ->
val unobfuscated = project.name.substringBefore('.').toInt() >= 26
val task = if (unobfuscated) "jar" else "remapJar"
project.tasks.matching { it.name == task }
}
dependsOn(modJars)
doLast { doLast {
val outputDir = layout.buildDirectory.dir("allJars").get().asFile val outputDir = layout.buildDirectory.dir("allJars").get().asFile
outputDir.mkdirs() outputDir.mkdirs()
remapJars.map { (it as AbstractArchiveTask).archiveFile.get().asFile } modJars.map { (it as AbstractArchiveTask).archiveFile.get().asFile }
.filter { it.exists() && !it.name.contains("sources") } .filter { it.exists() && !it.name.contains("sources") }
.forEach { it.copyTo(outputDir.resolve(it.name), overwrite = true) } .forEach { it.copyTo(outputDir.resolve(it.name), overwrite = true) }
println("All remapped jars collected in: $outputDir") println("All mod jars collected in: $outputDir")
} }
} }
+1 -2
View File
@@ -8,8 +8,7 @@ org.gradle.configuration-cache=false
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/develop # check these on https://fabricmc.net/develop
minecraft_version=1.21.10 minecraft_version=1.21.10
yarn_mappings=1.21.10+build.3 loader_version=0.19.5
loader_version=0.19.0
fabric_api_version=0.138.4+1.21.10 fabric_api_version=0.138.4+1.21.10
+1 -2
View File
@@ -8,8 +8,7 @@ org.gradle.configuration-cache=false
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/develop # check these on https://fabricmc.net/develop
minecraft_version=1.21.11 minecraft_version=1.21.11
yarn_mappings=1.21.11+build.4 loader_version=0.19.5
loader_version=0.19.0
fabric_api_version=0.141.3+1.21.11 fabric_api_version=0.141.3+1.21.11
-101
View File
@@ -1,101 +0,0 @@
plugins {
id 'net.fabricmc.fabric-loom-remap' version "${loom_version}"
id 'maven-publish'
}
version = "${project.mod_version}+${project.minecraft_version}"
group = project.maven_group
base {
archivesName = project.archives_base_name
}
repositories {
maven { name = "Selimaj"; url = 'https://git.selimaj.dev/api/packages/selimaj-dev/maven' }
}
dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
// Saturn's emote library (limb bending, Emotecraft emotes)
include modImplementation("org.saturnclient:emotes-fabric:${project.emotes_version}+${project.minecraft_version}")
implementation project(":common")
include implementation("dev.selimaj:session-java:0.2.0")
include implementation("de.javagl:obj:0.4.0")
include implementation("com.formdev:svgSalamander:1.1.4")
include implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
include implementation("com.fasterxml.jackson.core:jackson-annotations:2.17.0")
include implementation("com.fasterxml.jackson.core:jackson-core:2.17.0")
}
/*
* ================================================================
* Resource Processing
* ================================================================
* Injects version values into fabric.mod.json.
*/
processResources {
inputs.property "version", project.version
inputs.property "minecraft_version", project.minecraft_version
inputs.property "loader_version", project.loader_version
filteringCharset "UTF-8"
filesMatching("fabric.mod.json") {
expand(
"version": project.version,
"minecraft_version": project.minecraft_version,
"loader_version": project.loader_version
)
}
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 21
}
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
jar {
inputs.property "archivesName", project.base.archivesName
from("LICENSE") {
rename { "${it}_${inputs.properties.archivesName}"}
}
from project(":common").sourceSets.main.output
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
// configure the maven publication
publishing {
publications {
create("mavenJava", MavenPublication) {
artifactId = project.archives_base_name
from components.java
}
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}
+1 -3
View File
@@ -8,9 +8,7 @@ org.gradle.configuration-cache=false
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/develop # check these on https://fabricmc.net/develop
minecraft_version=1.21.4 minecraft_version=1.21.4
yarn_mappings=1.21.4+build.8 loader_version=0.19.5
loader_version=0.18.4
loom_version=1.17.21
# Mod Properties # Mod Properties
maven_group=org.saturnclient maven_group=org.saturnclient
-10
View File
@@ -1,10 +0,0 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
@@ -1,459 +0,0 @@
package org.saturnclient.impl.cosmetics;
import java.util.Arrays;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Config;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.equipment.EquipmentModel;
import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
import net.minecraft.client.render.entity.feature.FeatureRenderer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.EquippableComponent;
import net.minecraft.item.ItemStack;
import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.Vec3d;
/**
* The cloak renderer
* - **Efficient**: uses a image only consisting of the cloak texture, 16x8
* whitespace.
* - **Curving**: Curves when player velocity increases.
* - **Wavey**: Makes the cloak feel like wind, when jump sprinting.
*/
public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
private final EquipmentModelLoader equipmentModelLoader;
private static final int PARTS = 24;
private final float[] segmentValues = new float[PARTS];
private static final int H_PARTS = 6;
private float horizontalCurve = 0.0f;
private static final float TEX_W = 176f;
private static final float TEX_H = 138f;
private static final class PxRect {
final int x, y, w, h;
PxRect(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
float u1() {
return x / TEX_W;
}
float v1() {
return y / TEX_H;
}
float u2() {
return (x + w) / TEX_W;
}
float v2() {
return (y + h) / TEX_H;
}
}
private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128);
private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128);
private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128);
private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128);
private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context,
EquipmentModelLoader equipmentModelLoader) {
super(context);
this.equipmentModelLoader = equipmentModelLoader;
Arrays.fill(segmentValues, 0.0f);
}
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
EquippableComponent equippableComponent = (EquippableComponent) stack
.get(DataComponentTypes.EQUIPPABLE);
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
EquipmentModel equipmentModel = this.equipmentModelLoader
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get());
return !equipmentModel.getLayers(layerType).isEmpty();
} else {
return false;
}
}
private void vertex(VertexConsumer vc, Matrix4f posMat, Matrix3f normalMat,
Vec3d pos, int color,
float u, float v, int overlay, int light,
float nx, float ny, float nz) {
Vector4f p = new Vector4f((float) pos.x, (float) pos.y, (float) pos.z, 1.0f);
p.mul(posMat);
Vector3f n = new Vector3f(nx, ny, nz);
n.mul(normalMat);
vc.vertex(
p.x(), p.y(), p.z(),
color,
u, v,
overlay, light,
n.x(), n.y(), n.z());
}
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack.Entry entry,
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
float u1, float v1, float u2, float v2, int light, int overlay,
float normalX, float normalY, float normalZ, boolean flipWinding) {
// Extract matrix (you must transform positions manually now)
Matrix4f pos = entry.getPositionMatrix();
Matrix3f normalMat = entry.getNormalMatrix();
int color = 0xFFFFFFFF; // white RGBA
if (!flipWinding) {
// CCW
vertex(vertexConsumer, pos, normalMat, bottomLeft, color, u1, v2, overlay, light, normalX, normalY,
normalZ);
vertex(vertexConsumer, pos, normalMat, bottomRight, color, u2, v2, overlay, light, normalX, normalY,
normalZ);
vertex(vertexConsumer, pos, normalMat, topRight, color, u2, v1, overlay, light, normalX, normalY, normalZ);
vertex(vertexConsumer, pos, normalMat, topLeft, color, u1, v1, overlay, light, normalX, normalY, normalZ);
} else {
// CW
vertex(vertexConsumer, pos, normalMat, topLeft, color, u1, v1, overlay, light, normalX, normalY, normalZ);
vertex(vertexConsumer, pos, normalMat, topRight, color, u2, v1, overlay, light, normalX, normalY, normalZ);
vertex(vertexConsumer, pos, normalMat, bottomRight, color, u2, v2, overlay, light, normalX, normalY,
normalZ);
vertex(vertexConsumer, pos, normalMat, bottomLeft, color, u1, v2, overlay, light, normalX, normalY,
normalZ);
}
}
private void renderCape(MatrixStack.Entry entry, VertexConsumer vertexConsumer,
PlayerEntityRenderState playerState, int light, int overlay) {
float capeWidth = 10.0f / 16.0f;
float capeHeight = 16.0f / 16.0f;
float capeDepth = 1.0f / 16.0f;
final float frontU1 = FRONT_RECT.u1();
final float frontU2 = FRONT_RECT.u2();
final float frontV1 = FRONT_RECT.v1();
final float frontV2 = FRONT_RECT.v2();
final float frontPartV = (frontV2 - frontV1) / PARTS;
final float backU1 = BACK_RECT.u1();
final float backU2 = BACK_RECT.u2();
final float backV1 = BACK_RECT.v1();
final float backV2 = BACK_RECT.v2();
final float backPartV = (backV2 - backV1) / PARTS;
final float leftU1 = LEFT_EDGE_RECT.u1();
final float leftU2 = LEFT_EDGE_RECT.u2();
final float leftV1 = LEFT_EDGE_RECT.v1();
final float leftV2 = LEFT_EDGE_RECT.v2();
final float leftPartV = (leftV2 - leftV1) / PARTS;
final float rightU1 = RIGHT_EDGE_RECT.u1();
final float rightU2 = RIGHT_EDGE_RECT.u2();
final float rightV1 = RIGHT_EDGE_RECT.v1();
final float rightV2 = RIGHT_EDGE_RECT.v2();
final float rightPartV = (rightV2 - rightV1) / PARTS;
final float topU1 = TOP_RECT.u1();
final float topU2 = TOP_RECT.u2();
final float topV1 = TOP_RECT.v1();
final float topV2 = TOP_RECT.v2();
final float bottomU1 = BOTTOM_RECT.u1();
final float bottomU2 = BOTTOM_RECT.u2();
final float bottomV1 = BOTTOM_RECT.v1();
final float bottomV2 = BOTTOM_RECT.v2();
// =============================
// 1. Spine
// =============================
float[] spineY = new float[PARTS + 1];
float[] spineZ = new float[PARTS + 1];
float partHeight = capeHeight / PARTS;
for (int i = 0; i < PARTS; i++) {
float angle = (2.0f - segmentValues[i]) * ((float) Math.PI / 2f);
float dirY = -(float) Math.cos(angle);
float dirZ = (float) Math.sin(angle);
spineY[i + 1] = spineY[i] + dirY * partHeight;
spineZ[i + 1] = spineZ[i] + dirZ * partHeight;
}
// =============================
// 2. Grid
// =============================
Vec3d[][] inner = new Vec3d[H_PARTS + 1][PARTS + 1];
Vec3d[][] outer = new Vec3d[H_PARTS + 1][PARTS + 1];
for (int x = 0; x <= H_PARTS; x++) {
float t = (float) x / H_PARTS;
float xPos = -capeWidth / 2.0f + capeWidth * t;
float centered = t - 0.5f;
float curve = (centered * centered) * 0.3f;
float zCurve = (curve * horizontalCurve) / PARTS;
for (int i = 0; i <= PARTS; i++) {
float y = spineY[i];
float z = spineZ[i];
int idx = Math.min(i, PARTS - 1);
// angle: 0 is horizontal (y-flat), PI/2 is vertical (z-flat)
float angle = (2.0f - segmentValues[idx]) * ((float) Math.PI / 2f);
float cosA = (float) Math.cos(angle);
float sinA = (float) Math.sin(angle);
// Calculate how much the horizontal curve shifts the vertex
float currentCurve = zCurve * i;
// Rotate the curve offset so it aligns with the cape's tilt
float curveY = sinA * currentCurve;
float curveZ = cosA * currentCurve;
float thickY = sinA * capeDepth;
float thickZ = cosA * capeDepth;
inner[x][i] = new Vec3d(xPos, y - curveY, z - curveZ);
outer[x][i] = new Vec3d(xPos, (y - curveY) - thickY, (z - curveZ) - thickZ);
}
}
// =============================
// 3. Front + Back
// =============================
for (int x = 0; x < H_PARTS; x++) {
float uStart = frontU1 + (frontU2 - frontU1) * (1.0f - ((float) x / H_PARTS));
float uEnd = frontU1 + (frontU2 - frontU1) * (1.0f - ((float) (x + 1) / H_PARTS));
float buStart = backU1 + (backU2 - backU1) * ((float) x / H_PARTS);
float buEnd = backU1 + (backU2 - backU1) * ((float) (x + 1) / H_PARTS);
for (int i = 0; i < PARTS; i++) {
float vStart = frontV1 + frontPartV * i;
float vEnd = frontV1 + frontPartV * (i + 1);
float bvStart = backV1 + backPartV * i;
float bvEnd = backV1 + backPartV * (i + 1);
Vec3d innerTL = inner[x + 1][i];
Vec3d innerTR = inner[x][i];
Vec3d innerBL = inner[x + 1][i + 1];
Vec3d innerBR = inner[x][i + 1];
Vec3d outerTL = outer[x + 1][i];
Vec3d outerTR = outer[x][i];
Vec3d outerBL = outer[x + 1][i + 1];
Vec3d outerBR = outer[x][i + 1];
// FRONT
renderCapeQuad(vertexConsumer, entry,
outerBR, outerBL, outerTL, outerTR,
uStart, vStart, uEnd, vEnd,
light, overlay, 0, 0, 1, false);
// BACK
renderCapeQuad(vertexConsumer, entry,
innerBL, innerBR, innerTR, innerTL,
buEnd, bvStart, buStart, bvEnd,
light, overlay, 0, 0, -1, false);
}
}
// =============================
// 4. LEFT SIDE (x = H_PARTS)
// =============================
for (int i = 0; i < PARTS; i++) {
Vec3d innerTop = inner[H_PARTS][i];
Vec3d innerBot = inner[H_PARTS][i + 1];
Vec3d outerTop = outer[H_PARTS][i];
Vec3d outerBot = outer[H_PARTS][i + 1];
float vStart = leftV1 + leftPartV * i;
float vEnd = leftV1 + leftPartV * (i + 1);
renderCapeQuad(vertexConsumer, entry,
innerTop, outerTop, outerBot, innerBot,
leftU1, vEnd,
leftU2, vStart,
light, overlay, -1, 0, 0, false);
}
// =============================
// 5. RIGHT SIDE (x = 0)
// =============================
for (int i = 0; i < PARTS; i++) {
Vec3d innerTop = inner[0][i];
Vec3d innerBot = inner[0][i + 1];
Vec3d outerTop = outer[0][i];
Vec3d outerBot = outer[0][i + 1];
float vStart = rightV1 + rightPartV * i;
float vEnd = rightV1 + rightPartV * (i + 1);
renderCapeQuad(vertexConsumer, entry,
outerTop, innerTop, innerBot, outerBot,
rightU1, vEnd,
rightU2, vStart,
light, overlay, 1, 0, 0, false);
}
// =============================
// 6. TOP (i = 0)
// =============================
for (int x = 0; x < H_PARTS; x++) {
Vec3d outerL = outer[x][0];
Vec3d outerR = outer[x + 1][0];
Vec3d innerR = inner[x + 1][0];
Vec3d innerL = inner[x][0];
float uStart = topU1 + (topU2 - topU1) * ((float) x / H_PARTS);
float uEnd = topU1 + (topU2 - topU1) * ((float) (x + 1) / H_PARTS);
renderCapeQuad(vertexConsumer, entry,
outerL, outerR, innerR, innerL,
uStart, topV1, uEnd, topV2,
light, overlay, 0, 1, 0, false);
}
// =============================
// 7. BOTTOM (i = PARTS)
// =============================
for (int x = 0; x < H_PARTS; x++) {
Vec3d innerL = inner[x][PARTS];
Vec3d innerR = inner[x + 1][PARTS];
Vec3d outerR = outer[x + 1][PARTS];
Vec3d outerL = outer[x][PARTS];
float uStart = bottomU1 + (bottomU2 - bottomU1) * ((float) x / H_PARTS);
float uEnd = bottomU1 + (bottomU2 - bottomU1) * ((float) (x + 1) / H_PARTS);
renderCapeQuad(vertexConsumer, entry,
innerL, innerR, outerR, outerL,
uStart, bottomV1, uEnd, bottomV2,
light, overlay, 0, -1, 0, false);
}
}
// Shift array left and insert new value at the start
private void updateSegmentValues(float value) {
// Shift all elements one position to the right (from end to start)
for (int i = segmentValues.length - 1; i > 0; i--) {
segmentValues[i] = segmentValues[i - 1];
}
// Insert the new value at index 0
segmentValues[0] = value;
}
private long lastUpdate = 0;
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible
|| playerEntityRenderState.skinTextures.capeTexture() != null) {
return;
}
SaturnPlayer player = SaturnPlayer.get(playerEntityRenderState.name);
if (player == null) {
return;
}
IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak);
if (customCape == null
|| this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
return;
}
int minBrightness = 7;
int blockLight = (light >> 4) & 0xF;
int skyLight = (light >> 20) & 0xF;
blockLight = Math.max(blockLight, minBrightness);
skyLight = Math.max(skyLight, minBrightness);
light = (skyLight << 20) | (blockLight << 4);
VertexConsumer vertexConsumer = vertexConsumerProvider
.getBuffer(ShaderUtils.getRenderLayer(customCape));
matrixStack.push();
matrixStack.translate(0.0f, 0.0f, 0.12f);
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) {
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
}
if (playerEntityRenderState.isInSneakingPose) {
matrixStack.translate(0, 0.16f, 0.0f);
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f));
}
long now = System.currentTimeMillis();
if (now - lastUpdate >= 20) {
float velX = Math.min(0.8f, playerEntityRenderState.field_53537 / 108.0f);
float rawVelY = playerEntityRenderState.field_53536;
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
float value = Math.max(0.0f, Math.min(2.0f, playerEntityRenderState.isSwimming ? 0.0f : velX + velY));
if (Config.cloakPhysics.value) {
updateSegmentValues(segmentValues[0] + (value - segmentValues[0]) * 0.3f);
} else {
Arrays.fill(segmentValues, segmentValues[0] + (value - segmentValues[0]) * 0.05f);
}
lastUpdate = now;
}
horizontalCurve = MathHelper.clamp(playerEntityRenderState.field_53537 / 150.0f, -1.0f, 1.0f);
renderCape(matrixStack.peek(), vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
matrixStack.pop();
}
}
@@ -1,39 +0,0 @@
package org.saturnclient.impl.cosmetics;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.impl.cosmetics.obj.MtlLoader;
import org.saturnclient.impl.cosmetics.obj.ObjModel;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.feature.FeatureRenderer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
super(context);
}
@Override
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
PlayerEntityRenderState state, float limbAngle, float limbDistance) {
SaturnPlayer player = SaturnPlayer.get(state.name);
if (state.invisible || player == null || player.hat.isEmpty()) {
return;
}
matrices.push();
this.getContextModel().head.rotate(matrices);
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, vertexConsumers, light,
OverlayTexture.DEFAULT_UV);
matrices.pop();
}
}
@@ -1,56 +0,0 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.javagl.obj.*;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.Identifier;
public class MtlLoader {
static Map<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>();
static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
.getResource(mtlId).get().getInputStream()) {
List<Mtl> mtlList = MtlReader.read(is);
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
for (Mtl mtl : mtlList) {
mtlMap.put(mtl.getName(), mtl);
}
return mtlMap;
}
}
public static Map<String, Mtl> of(Identifier mtlId) {
if (loadedMtls.containsKey(mtlId))
return loadedMtls.get(mtlId);
try {
Map<String, Mtl> mtl = loadMtl(mtlId);
loadedMtls.put(mtlId, mtl);
return mtl;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static Map<String, Mtl> cosmetic(String cosmeticId) {
int index = cosmeticId.indexOf('_');
String mtlId = (index == -1) ? "" : cosmeticId.substring(index + 1);
if (mtlId.isEmpty())
mtlId = "white";
return of(Identifier.of("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl"));
}
}
@@ -1,102 +0,0 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.javagl.obj.Mtl;
import de.javagl.obj.Obj;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.resource.Resource;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
public class ObjModel {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static final Map<Identifier, ObjModel> loadedObjModels = new HashMap<>();
public record Config(
@JsonProperty("translate") Vec3d transform,
@JsonProperty("scale") Vec3d scale,
@JsonProperty("rotation") Vec3d rotation) {
public Config {
transform = transform == null ? new Vec3d(0, 0, 0) : transform;
scale = scale == null ? new Vec3d(1, 1, 1) : scale;
rotation = rotation == null ? new Vec3d(0, 0, 0) : rotation;
}
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
public record Vec3d(float x, float y, float z) {
}
}
Obj obj;
Config config;
public static ObjModel cosmetic(String cosmeticKind, String id) {
return of(Identifier.of("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model"));
}
public static ObjModel of(Identifier obj) {
if (loadedObjModels.containsKey(obj))
return loadedObjModels.get(obj);
ObjModel o = new ObjModel(obj);
loadedObjModels.put(obj, o);
return o;
}
ObjModel(Identifier identifier) {
Identifier objId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".obj");
Identifier jsonId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".json");
try {
this.obj = ObjRenderer.loadObj(objId);
} catch (IOException e) {
e.printStackTrace();
}
// Try loading JSON config if it exists
ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager();
Optional<Resource> resource = resourceManager.getResource(jsonId);
if (resource.isPresent()) {
try (InputStream stream = resource.get().getInputStream()) {
this.config = MAPPER.readValue(stream, Config.class);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void render(Map<String, Mtl> mtlMap, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
int overlay) {
matrices.push();
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
if (this.config != null) {
matrices.translate(config.transform.x, config.transform.y, config.transform.z);
matrices.scale(config.scale.x, config.scale.y, config.scale.z);
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(config.rotation.x));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(config.rotation.y));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Z.rotationDegrees(config.rotation.z));
}
ObjRenderer.renderObj(this.obj, mtlMap, matrices, vertexConsumers, light, overlay);
matrices.pop();
}
}
@@ -1,112 +0,0 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import de.javagl.obj.*;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
public class ObjRenderer {
public static Obj loadObj(Identifier objId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
.getResource(objId).get().getInputStream()) {
return ObjReader.read(is);
}
}
public static void renderObj(Obj obj, Map<String, Mtl> mtlMap, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light, int overlay) {
MatrixStack.Entry entry = matrices.peek();
Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
for (Map.Entry<String, Obj> group : materialGroups.entrySet()) {
String materialName = group.getKey();
Obj groupObj = group.getValue();
Mtl mtl = mtlMap != null ? mtlMap.get(materialName) : null;
// Resolve texture identifier using same logic as ObjUnbakedModelModel
IdentifierRef texture = IdentifierRef.ofVanilla("textures/misc/white.png");
if (mtl != null) {
String mapKd = mtl.getMapKd();
if (mapKd != null) {
if (!mapKd.startsWith("#")) {
texture = IdentifierRef.of(mapKd);
}
}
}
// Diffuse color (Kd)
float r = 1f, g = 1f, b = 1f;
if (mtl != null) {
FloatTuple kd = mtl.getKd();
if (kd != null) {
r = kd.getX();
g = kd.getY();
b = kd.getZ();
}
}
VertexConsumer consumer = vertexConsumers.getBuffer(ShaderUtils.getRenderLayer(texture));
int faces = groupObj.getNumFaces();
final float fr = r, fg = g, fb = b;
for (int f = 0; f < faces; f++) {
ObjFace face = groupObj.getFace(f);
int verts = face.getNumVertices();
// Your original winding order logic, untouched
for (int i = verts - 1; i >= 0; i--) {
writeVertex(groupObj, face, i, consumer, entry, light, overlay, fr, fg, fb);
}
}
}
}
private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
MatrixStack.Entry matrix, int light, int overlay,
float r, float g, float b) {
FloatTuple pos = obj.getVertex(face.getVertexIndex(index));
float x = pos.getX();
float y = pos.getY();
float z = pos.getZ();
float nx = 0f, ny = 1f, nz = 0f;
int normalIndex = face.getNormalIndex(index);
if (normalIndex >= 0) {
FloatTuple n = obj.getNormal(normalIndex);
nx = n.getX();
ny = n.getY();
nz = n.getZ();
}
float u = 0f, v = 0f;
int texIndex = face.getTexCoordIndex(index);
if (texIndex >= 0) {
FloatTuple tc = obj.getTexCoord(texIndex);
u = tc.getX();
v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left
}
consumer.vertex(matrix, x, y, z)
.color(r, g, b, 1f)
.texture(u, v)
.overlay(overlay)
.light(light)
.normal(matrix, nx, ny, nz);
}
}
@@ -1,68 +0,0 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.util.Identifier;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Map;
public class IdentifierUtils {
public static void registerBufferedImageTextureFast(IdentifierRef identifierRef, BufferedImage bufferedImage) {
try {
Identifier identifier = (Identifier) (Object) identifierRef;
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, pixels[index++]);
}
}
} else {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, bufferedImage.getRGB(x, y));
}
}
}
SaturnClient.client.execute(() -> {
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
SaturnClient.client.getTextureManager().registerTexture(identifier, texture);
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register texture: {}", identifierRef, e);
}
}
public static void registerBufferedImageTexturesBulk(Map<IdentifierRef, BufferedImage> textures) {
long start = System.currentTimeMillis();
textures.forEach(IdentifierUtils::registerBufferedImageTextureFast);
long end = System.currentTimeMillis();
SaturnClient.LOGGER.info(
"Registered {} textures in {}ms",
textures.size(),
(end - start));
}
}
@@ -1,41 +0,0 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.cosmetics.utils.GifUtils;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderPhase;
import net.minecraft.client.render.VertexFormat.DrawMode;
import net.minecraft.client.render.VertexFormats;
import net.minecraft.client.render.RenderPhase.Textures;
import net.minecraft.client.render.block.entity.EndPortalBlockEntityRenderer;
import net.minecraft.util.Identifier;
public class ShaderUtils {
public static final RenderLayer END_GATEWAY = RenderLayer.of(
"end_gateway",
VertexFormats.POSITION,
DrawMode.QUADS,
1536,
false,
false,
RenderLayer.MultiPhaseParameters.builder()
.program(RenderLayer.END_GATEWAY_PROGRAM)
.texture(Textures.create()
.add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false, false)
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false, false)
.build())
.cull(RenderPhase.DISABLE_CULLING)
.build(false));
public static RenderLayer getRenderLayer(IdentifierRef texture) {
String path = texture.toString();
if (path.endsWith(".gif")) {
return RenderLayer.getEntityAlpha((Identifier) (Object) GifUtils.get(texture));
} else if (path.endsWith("end.png")) {
return END_GATEWAY;
} else {
return RenderLayer.getEntityAlpha((Identifier) (Object) texture);
}
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.impl.features.entity;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -1,79 +0,0 @@
package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.entity.LivingEntity;
/**
* Fabric implementation of {@link EntityFeature}.
*
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* Entity state for nametag rendering is supplied at render time by
* {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which
* writes into the render state; {@link NametagsMixin} then constructs
* a {@link RenderStateEntityState} from the already-extracted snapshot.
*/
public class EntityFeatureImpl implements EntityFeature {
private final MinecraftClient mc;
public EntityFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean isTargetingLivingEntity() {
HitResult target = mc.crosshairTarget;
if (target == null || target.getType() != HitResult.Type.ENTITY)
return false;
Entity hit = ((EntityHitResult) target).getEntity();
return hit instanceof LivingEntity;
}
// ---------------------------------------------------------------
// Render-state adapter
// ---------------------------------------------------------------
/**
* Wraps a {@link HealthRenderState} (already injected into the
* Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}.
*
* This is constructed inside {@link NametagsMixin} where both the
* render-state mixin interface and the entity name are available.
*/
public static final class RenderStateEntityState implements EntityState {
private final String customName;
private final HealthRenderState hrs;
public RenderStateEntityState(String customName, HealthRenderState hrs) {
this.customName = customName;
this.hrs = hrs;
}
@Override
public String getCustomName() {
return customName;
}
@Override
public float getHealth() {
return hrs.saturn$getHealth();
}
@Override
public float getMaxHealth() {
return hrs.saturn$getMaxHealth();
}
@Override
public EntityType getEntityType() {
return hrs.saturn$getEntityType();
}
}
}
@@ -1,24 +0,0 @@
package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature;
/**
* Injected interface on
* {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
*
* Carries the entity's health snapshot and classification into the render
* pipeline so that {@link NametagsMixin} can read them without touching
* the live entity again.
*/
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityFeature.EntityType saturn$getEntityType();
void saturn$setEntityType(EntityFeature.EntityType type);
}
@@ -1,46 +0,0 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing,
float tickDelta, CallbackInfo ci) {
if (FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && SaturnClient.client.player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getPitch());
cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYaw());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(),
cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -1,62 +0,0 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.mod.mods.FreelookMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
// noinspection ConstantValue// IntelliJ is incorrect here, this code block is
// reachable
if (FreelookMod.isFreeLooking() && (Object) this instanceof ClientPlayerEntity) {
double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15);
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta;
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return this.cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return this.cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float pitch) {
this.cameraPitch = pitch;
}
@Override
@Unique
public void freelook$setCameraYaw(float yaw) {
this.cameraYaw = yaw;
}
}
@@ -1,95 +0,0 @@
package org.saturnclient.impl.features.mixins.misc;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.render.LivingEntityRenderStateMixin;
import org.saturnclient.mod.mods.NametagsMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Intercepts nametag rendering and replaces the displayed text with
* the string produced by {@link NametagsMod#getNametagString}.
*
* Key changes from the original:
*
* <ul>
* <li>The old {@code NametagsFabric.EntityStateImpl} bridge class is
* replaced by {@link EntityFeatureImpl.RenderStateEntityState},
* which wraps the {@link HealthRenderState} already injected into
* the render state by {@link LivingEntityRenderStateMixin}.</li>
* <li>The feature import changes from {@code Nametags} to
* {@link NametagsMod}; the two static methods
* ({@code shouldReplaceName} / {@code getNametagString}) are
* unchanged in signature.</li>
* <li>The entity's display name is extracted from the render state's
* {@code nameVisible} / text fields where available, falling back
* to the plain text string passed to this method.</li>
* </ul>
*
* The re-entrancy guard ({@code saturn$rendering}) is kept intact to
* prevent infinite recursion when we call {@code renderLabelIfPresent}
* with the replacement text.
*/
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
/** Guards against the recursive call we make with the replacement text. */
@Unique
private static final ThreadLocal<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
@Shadow
protected abstract <S extends EntityRenderState> void renderLabelIfPresent(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light);
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
private <S extends EntityRenderState> void saturn$replaceNametag(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light, CallbackInfo ci) {
// Skip our own recursive call
if (saturn$rendering.get())
return;
if (!NametagsMod.shouldReplaceName())
return;
// Only living entities carry health data
if (!(state instanceof LivingEntityRenderState living))
return;
if (!(living instanceof HealthRenderState hrs))
return;
// Build the platform-neutral EntityState from the render-state snapshot
// (health/type were written by LivingEntityRendererMixin at extract time).
String customName = text != null ? text.getString() : null;
EntityFeatureImpl.RenderStateEntityState entityState = new EntityFeatureImpl.RenderStateEntityState(
customName, hrs);
// Ask the feature for the replacement string
String replacement = NametagsMod.getNametagString(entityState);
if (replacement == null)
return; // feature decided not to replace this entity
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
}
@@ -1,21 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.BackgroundRenderer;
import org.saturnclient.mod.mods.NoFogMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(BackgroundRenderer.class)
public class BackgroundRendererMixin {
// fog
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFogMod.isActive()) {
return viewDistance * 2.0f;
}
return viewDistance;
}
}
@@ -1,41 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.GameRenderer;
import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.mod.mods.ZoomMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Overrides the game's field-of-view when {@link ZoomMod} is active.
*
* The old code checked both {@code Zoom.isZooming} and
* {@code Zoom.shouldZoom()} separately. The refactored
* {@link ZoomMod#shouldZoom()} already ORs both conditions
* ({@code enabled.value && isZooming}), so only one check is needed.
*
* No {@link org.saturnclient.common.module.ModProvider} is required
* here because zooming is a pure render-time override with no per-tick
* engine queries.
*/
@Mixin(GameRenderer.class)
public class GameRendererMixin {
/**
* After the vanilla FOV has been computed, divide it by the zoom
* level to produce the narrowed field-of-view.
*/
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (ZoomMod.shouldZoom()) {
float zoomedFov = cir.getReturnValue() / ZoomMod.getZoomLevel();
cir.setReturnValue(zoomedFov);
} else if (FreelookMod.shouldZoomOut()) {
float zoomedFov = cir.getReturnValue() / FreelookMod.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -1,35 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager;
import org.saturnclient.mod.mods.FullbrightMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Overrides the lightmap's gamma computation when
* {@link FullbrightMod} is active, replacing the vanilla
* brightness with a user-configured value.
*
* The old import was {@code org.saturnclient.Mod.Mods.Fullbright};
* it is now {@link FullbrightMod}. The injection target and
* strategy are unchanged.
*/
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
/**
* If fullbright is active, substitute the Mod's brightness
* value for whatever vanilla calculated.
*/
@ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
private float modifyGamma(float original) {
if (FullbrightMod.shouldOverrideBrightness()) {
return FullbrightMod.getBrightnessValue();
}
return original;
}
}
@@ -1,60 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.misc.NametagsMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Implements {@link HealthRenderState} on
* {@link LivingEntityRenderState}, injecting health and entity-type
* storage into every living entity's render-state snapshot.
*
* Previously imported {@code NametagsInterface.EntityType}; the enum
* now lives in {@link EntityFeature.EntityType} which is the single
* canonical definition shared by the mixin layer and the feature layer.
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by
* {@link NametagsMixin} at render time.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@Unique
private float saturn$health = 0f;
@Unique
private float saturn$maxHealth = 1f;
@Unique
private EntityFeature.EntityType saturn$entityType = EntityFeature.EntityType.OTHER;
@Override
public float saturn$getHealth() {
return saturn$health;
}
@Override
public float saturn$getMaxHealth() {
return saturn$maxHealth;
}
@Override
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
@Override
public EntityFeature.EntityType saturn$getEntityType() {
return saturn$entityType;
}
@Override
public void saturn$setEntityType(EntityFeature.EntityType type) {
saturn$entityType = type;
}
}
@@ -1,51 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.passive.PassiveEntity;
import net.minecraft.entity.player.PlayerEntity;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Captures each living entity's health and type into the render state
* during {@code updateRenderState}, so that nametag rendering can read
* them without touching the live entity again.
*
* Previously imported {@code NametagsInterface.EntityType}; now uses
* {@link EntityFeature.EntityType} — the canonical enum shared across
* the whole module system.
*
* The injection target and strategy are unchanged from the original.
*/
@Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
@Inject(method = "updateRenderState", at = @At("TAIL"))
private void saturn$captureHealth(T entity, S state, float tickDelta, CallbackInfo ci) {
if (!(state instanceof HealthRenderState hrs))
return;
hrs.saturn$setHealth(entity.getHealth(), entity.getMaxHealth());
// Classify entity type for the nametag feature's filtering logic
EntityFeature.EntityType type;
if (entity instanceof PlayerEntity)
type = EntityFeature.EntityType.PLAYER;
else if (entity instanceof HostileEntity)
type = EntityFeature.EntityType.HOSTILE;
else if (entity instanceof PassiveEntity)
type = EntityFeature.EntityType.PASSIVE;
else
type = EntityFeature.EntityType.OTHER;
hrs.saturn$setEntityType(type);
}
}
@@ -1,85 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
import org.saturnclient.impl.ui.RenderScopeImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.screens.HudEditor;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Drives per-frame rendering for all registered features.
*
* This mixin is intentionally kept thin: it creates a {@link RenderScope},
* iterates the enabled feature list from {@link ModManager}, and
* dispatches either {@link HudMod#renderHud} (for HUD elements) or
* {@link Mod#render} (for world-overlay features like Crosshair).
*
* No feature-specific logic lives here — all decisions about what to
* draw are made inside each feature class.
*
* Changes from the original:
* - Package imports updated to the refactored feature names (no
* more {@code feature.features.*}).
* - The HUD-editor screen check still uses the existing
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
* unchanged infrastructure.
*/
@Mixin(InGameHud.class)
public class RenderMixin {
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
// Skip rendering while the HUD editor is open so the editor
// can draw its own preview layout without double-drawing.
if (textRenderer == null)
return;
if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f
&& f.screen instanceof HudEditor)
return;
RenderScope scope = new RenderScopeImpl(
context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (Mod feature : ModManager.ENABLED_MODS) {
if (!feature.isEnabled())
continue;
if (feature instanceof HudMod hud) {
ModLayout dim = hud.getDimensions();
scope.getMatrixStack().push();
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value);
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value);
if (dim.renderBackground) {
scope.drawRoundedRectangle(0, 0, dim.width, dim.height,
dim.radius.value, dim.bgColor.value);
}
hud.renderHud(scope);
scope.getMatrixStack().pop();
} else {
// Non-HUD features (e.g. CrosshairMod) use the plain render hook.
feature.render(scope);
}
}
}
}
@@ -1,30 +0,0 @@
package org.saturnclient.impl.features.mixins.world;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
import org.saturnclient.mod.mods.TpsMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Feeds world-time packets into {@link TpsMod}'s TPS estimator.
*
* This is the only place in the entire codebase that reads a network
* packet and passes data to a feature. The feature's
* {@link TpsMod#onTimePacket(long)} method handles all averaging
* logic; the mixin is intentionally reduced to a single forwarding call.
*
* The only change from the original is renaming the import from
* {@code feature.features.Tps} to {@link TpsMod}.
*/
@Mixin(ClientPlayNetworkHandler.class)
public class TpsMixin {
@Inject(method = "onWorldTimeUpdate", at = @At("HEAD"))
private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) {
TpsMod.onTimePacket(packet.time());
}
}
@@ -1,37 +0,0 @@
package org.saturnclient.impl.features.network;
import org.saturnclient.common.feature.NetworkFeature;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.PlayerListEntry;
/**
* Fabric implementation of {@link NetworkFeature}.
*
* Connection presence is checked via the network handler.
* Ping is read from the player's {@link PlayerListEntry} which the
* server populates; returns {@code -1} when unavailable.
*/
public class NetworkFeatureImpl implements NetworkFeature {
private final MinecraftClient mc;
public NetworkFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasNetwork() {
return mc.getNetworkHandler() != null;
}
@Override
public int getPing() {
if (mc.player == null || mc.getNetworkHandler() == null)
return -1;
PlayerListEntry entry = mc.getNetworkHandler().getPlayerListEntry(mc.player.getUuid());
return entry != null ? entry.getLatency() : -1;
}
}
@@ -1,253 +0,0 @@
package org.saturnclient.impl.features.player;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.util.Identifier;
import org.saturnclient.common.feature.PlayerFeature;
import org.saturnclient.common.ref.game.EffectRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Fabric implementation of {@link PlayerFeature}.
*
* All access to {@link MinecraftClient} and {@link ClientPlayerEntity}
* is encapsulated here. Features only ever see the {@link PlayerFeature}
* interface; they never import Minecraft classes directly.
*
* Every method guards against a missing player and returns a safe
* default so features are free of null checks.
*/
public class PlayerFeatureImpl implements PlayerFeature {
private final MinecraftClient mc;
public PlayerFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Internal helper — never returns null
// ---------------------------------------------------------------
/** Returns the local player, or {@code null} if one is not loaded. */
private ClientPlayerEntity player() {
return mc.player;
}
// ---------------------------------------------------------------
// Presence
// ---------------------------------------------------------------
@Override
public boolean hasPlayer() {
return mc.player != null;
}
// ---------------------------------------------------------------
// Position
// ---------------------------------------------------------------
@Override
public int getX() {
return hasPlayer() ? (int) player().getX() : 0;
}
@Override
public int getY() {
return hasPlayer() ? (int) player().getY() : 0;
}
@Override
public int getZ() {
return hasPlayer() ? (int) player().getZ() : 0;
}
// ---------------------------------------------------------------
// Input / movement
// ---------------------------------------------------------------
@Override
public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed();
}
@Override
public boolean isBackPressed() {
return mc.options.backKey.isPressed();
}
@Override
public boolean isLeftPressed() {
return mc.options.leftKey.isPressed();
}
@Override
public boolean isRightPressed() {
return mc.options.rightKey.isPressed();
}
@Override
public boolean isJumpPressed() {
return mc.options.jumpKey.isPressed();
}
@Override
public boolean isAttackPressed() {
return mc.options.attackKey.isPressed();
}
@Override
public boolean isUsePressed() {
return mc.options.useKey.isPressed();
}
@Override
public boolean isSneaking() {
return hasPlayer() && player().isSneaking();
}
@Override
public boolean isUsingItem() {
return hasPlayer() && player().isUsingItem();
}
@Override
public boolean hasHorizontalCollision() {
return hasPlayer() && player().horizontalCollision;
}
@Override
public boolean isOnGround() {
return hasPlayer() && player().isOnGround();
}
@Override
public void setSprinting(boolean sprinting) {
if (hasPlayer())
player().setSprinting(sprinting);
}
// ---------------------------------------------------------------
// Velocity
// ---------------------------------------------------------------
@Override
public Velocity getVelocity() {
if (!hasPlayer())
return Velocity.ZERO;
var v = player().getVelocity();
return new Velocity(v.x, v.y, v.z);
}
// ---------------------------------------------------------------
// Stats
// ---------------------------------------------------------------
@Override
public float getHealth() {
return hasPlayer() ? player().getHealth() : 0f;
}
// ---------------------------------------------------------------
// Equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getMainHand() {
return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY);
}
@Override
public ItemStackRef getHelmet() {
return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getChestplate() {
return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getLeggings() {
return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getBoots() {
return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
}
// ---------------------------------------------------------------
// Dummy equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getDummyMainHand() {
return wrap(new ItemStack(Items.DIAMOND_SWORD));
}
@Override
public ItemStackRef getDummyHelmet() {
return wrap(new ItemStack(Items.DIAMOND_HELMET));
}
@Override
public ItemStackRef getDummyChestplate() {
return wrap(new ItemStack(Items.DIAMOND_CHESTPLATE));
}
@Override
public ItemStackRef getDummyLeggings() {
return wrap(new ItemStack(Items.DIAMOND_LEGGINGS));
}
@Override
public ItemStackRef getDummyBoots() {
return wrap(new ItemStack(Items.DIAMOND_BOOTS));
}
// ---------------------------------------------------------------
// Status effects
// ---------------------------------------------------------------
@Override
public List<? extends EffectRef> getActiveEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<EffectRef> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add((EffectRef) instance);
}
return result;
}
@Override
public List<? extends EffectRef> getDummyEffects() {
return List.of(
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2));
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static ItemStackRef wrap(ItemStack stack) {
return (ItemStackRef) (Object) stack;
}
}
@@ -1,64 +0,0 @@
package org.saturnclient.impl.features.render;
import org.saturnclient.common.feature.RenderFeature;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.Perspective;
/**
* Fabric implementation of {@link RenderFeature}.
*
* Window dimensions and FPS are read directly from
* {@link MinecraftClient}. Camera perspective control delegates to
* {@link net.minecraft.client.option.GameOptions}.
*/
public class RenderFeatureImpl implements RenderFeature {
private final MinecraftClient mc;
public RenderFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Window
// ---------------------------------------------------------------
@Override
public int getScaledWindowWidth() {
return mc.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return mc.getWindow().getScaledHeight();
}
// ---------------------------------------------------------------
// Performance
// ---------------------------------------------------------------
@Override
public int getFps() {
return mc.getCurrentFps();
}
// ---------------------------------------------------------------
// Camera perspective
// ---------------------------------------------------------------
@Override
public boolean isFirstPerson() {
return mc.options.getPerspective() == Perspective.FIRST_PERSON;
}
@Override
public void setFirstPerson() {
mc.options.setPerspective(Perspective.FIRST_PERSON);
}
@Override
public void setThirdPersonBack() {
mc.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
}
@@ -1,36 +0,0 @@
package org.saturnclient.impl.features.world;
import org.saturnclient.common.feature.WorldFeature;
import net.minecraft.client.MinecraftClient;
/**
* Fabric implementation of {@link WorldFeature}.
*
* Reads the client world's time directly from {@link MinecraftClient}.
* Returns safe defaults when no world is loaded.
*/
public class WorldFeatureImpl implements WorldFeature {
private final MinecraftClient mc;
public WorldFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasWorld() {
return mc.world != null;
}
/**
* Returns the total world age in ticks, or {@code 0} when no world
* is loaded. Used by {@link TpsFeature#onTimePacket} (which is
* called from the mixin and stores its own copy) and by
* {@link DayCounterFeature} for the day count.
*/
@Override
public long getWorldAge() {
return hasWorld() ? mc.world.getTime() : 0L;
}
}
@@ -1,13 +0,0 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.util.Identifier;
@Mixin(Identifier.class)
public abstract class IdentifierMixin implements IdentifierRef {
@Shadow
public abstract String toString();
}
@@ -1,19 +0,0 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.item.ItemStack;
@Mixin(ItemStack.class)
public abstract class ItemStackMixin implements ItemStackRef {
@Shadow
public abstract boolean isEmpty();
@Shadow
public abstract int getMaxDamage();
@Shadow
public abstract int getDamage();
}

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