Compare commits

..
Author SHA1 Message Date
selimaj-dev 23e508138b Screenshot and fixed small animation duration 2026-02-25 18:01:47 +01:00
selimaj-dev 593b9f4760 Merge pull request #30 from saturnclientmc/cloak-physics
Cloak physics #14
2026-02-25 08:51:23 -08:00
selimaj-dev 5b44caa358 Falling physics 2026-02-25 17:47:49 +01:00
selimaj-dev 28d71e63a9 Flat cloak 2026-02-25 17:35:50 +01:00
selimaj-dev 84addc7996 Fixed gaps in curve 2026-02-25 17:11:38 +01:00
selimaj-dev 3f8d68619a Working simulation 2026-02-25 16:52:12 +01:00
selimaj-dev d941334fa3 Fixed gap 2026-02-25 16:34:06 +01:00
selimaj-dev 9c36d33cb2 Working ish system 2026-02-25 16:24:45 +01:00
selimaj-dev 6b03a1441a Fixing the rendering anchoring 2026-02-25 16:06:10 +01:00
selimaj-dev f40d3026e6 Fixed reverse rendering and position 2026-02-25 14:41:22 +01:00
selimaj-dev 2ea122c4a9 Working full cape rendering 2026-02-25 14:17:57 +01:00
selimaj-dev dfce271c37 Cape segment specific rendering 2026-02-25 13:52:07 +01:00
Rudi d40724205c Merge pull request #29 from RudideC/master
bugfixes, changes to animation defaults
2026-02-25 11:05:58 +01:00
RudideC 55afd1e436 changed default font
i think this looks better. thoughts?
2026-02-25 11:03:03 +01:00
RudideC eb3e3d61db changed default animation timings
should be snappier now
2026-02-25 11:02:40 +01:00
RudideC 5da0e3c428 added myself as contributor 2026-02-25 11:02:21 +01:00
RudideC f464d10971 fixed libraries not included 2026-02-25 11:02:08 +01:00
RudideC 6e0bc22c17 bugfix, fixed mod not loading
remap = false makes it so that it works when developing, but not when compiled
2026-02-25 11:01:57 +01:00
selimaj-dev bb95999112 Implementing cape curving 2026-02-25 04:34:57 +01:00
selimaj-dev 9c0f92d034 Fixed title screen 2026-02-25 03:47:05 +01:00
selimaj-dev 9d04958ff2 Merge pull request #28 from saturnclientmc/animation-settings
Animation settings
2026-02-24 18:36:53 -08:00
selimaj-dev cfe0d4a97f Emote wheel and cosmetics animations 2026-02-25 02:07:30 +01:00
selimaj-dev 0995484c09 Emote wheel animation config 2026-02-25 01:28:52 +01:00
selimaj-dev ec2254cf74 Implemented curves for all animations 2026-02-25 01:18:00 +01:00
selimaj-dev 71ae403188 Implementing AnimationConfig directly 2026-02-25 00:54:35 +01:00
selimaj-dev 6689919b3b Fixes and implemented animation curves 2026-02-25 00:39:48 +01:00
selimaj-dev 246e49e521 Animation fix 2026-02-25 00:25:13 +01:00
selimaj-dev eb065b20d7 Animation config 2026-02-25 00:18:16 +01:00
Rudi 038c859133 Merge pull request #27 from RudideC/master
0.0.7-beta1
2026-02-24 21:41:47 +01:00
RudideC 434afa7f62 0.0.7-beta1
all code integrated, beta release is ready
2026-02-24 21:38:40 +01:00
RudideC fd492b91a0 animation fine-tuning, easing change
can change between ease-in-out and ease-out (default)
2026-02-24 21:34:56 +01:00
RudideC 48a41af3b0 changed timings
personal preference, i think these should be a setting
2026-02-24 21:27:46 +01:00
RudideC 6da9eb55ba fixed bug, where modmenu wouldn't have scroll 2026-02-24 21:27:23 +01:00
RudideC 22efd8b6e0 moved to svg
yay
2026-02-24 16:23:34 +01:00
Rudi b71d779900 Merge pull request #25 from RudideC/master
Nametags, Ping, Clock, Tps
2026-02-24 10:05:52 +01:00
Rudi 6c3dbd90a0 Merge branch 'master' into master 2026-02-24 10:05:15 +01:00
RudideC 1b16098121 0.0.7-alpha1
will do some testing and then move towards the beta release
2026-02-24 09:41:50 +01:00
RudideC 092fcfb009 new icons
wow
2026-02-24 09:40:06 +01:00
RudideC 008a0927b6 clock mod 2026-02-24 09:37:37 +01:00
RudideC 3c2f75e438 tps mod
missing the image, will add later
2026-02-24 09:21:23 +01:00
RudideC 09c354337d rename 2026-02-24 09:19:17 +01:00
selimaj-dev dc6387cc27 Merge pull request #24 from saturnclientmc/services-and-ui-tweaks
Services and UI tweaks
2026-02-23 12:32:03 -08:00
selimaj-dev bff18a05d1 Changed ip to the actual server 2026-02-23 21:30:50 +01:00
selimaj-dev 6717d7e901 Fixed UUID and cloak 2026-02-23 18:51:28 +01:00
selimaj-dev bbf4b17494 Attempting to fix sendplayer 2026-02-23 17:46:28 +01:00
selimaj-dev 68b4bfc439 Working peer player 2026-02-23 16:29:48 +01:00
RudideC 77f5350496 forgot to add icon 2026-02-23 11:38:57 +01:00
RudideC 3c1914c30a added ping mod
wow
2026-02-23 11:27:34 +01:00
RudideC ae8ec4b4ef fixed modmenu scroll 2026-02-23 11:27:25 +01:00
RudideC bdd421e1da nametag module finished
yay
2026-02-23 10:20:41 +01:00
RudideC 7b480d1b8b unit
wow
2026-02-23 09:52:42 +01:00
selimaj-dev 2a425335f0 Working emote streaming 2026-02-23 09:34:38 +01:00
RudideC 02f8b486f3 obfuscate names, name color 2026-02-23 09:24:27 +01:00
selimaj-dev f0966c86f3 Fixing emote system 2026-02-23 09:20:10 +01:00
RudideC 65c6c2a284 colors, can hide heart emoji 2026-02-23 09:11:06 +01:00
RudideC 9fe05722f9 player, hostile, passive filter 2026-02-23 08:47:02 +01:00
selimaj-dev 61be7c317f Fixed the hat rendering issue 2026-02-23 07:22:44 +01:00
selimaj-dev df35497af2 Trying to find the hat rendering issue 2026-02-23 07:18:54 +01:00
selimaj-dev 8614e1bdd1 Working service system 2026-02-23 06:47:08 +01:00
selimaj-dev c5c38247cc Improved build.gradle 2026-02-23 03:45:26 +01:00
selimaj-dev f9c0d3fe58 Fixed keystrokes 2026-02-23 00:24:14 +01:00
selimaj-dev 44c0b69c4d Fixed keystrokes 2026-02-22 23:59:25 +01:00
selimaj-dev 0cfa7b81ef SVG mod icons 2026-02-22 23:40:39 +01:00
selimaj-dev 9caf47d81b Fixed svg issues 2026-02-22 18:11:58 +01:00
selimaj-dev 3c27d3b377 Working svg renderer 2026-02-22 16:41:37 +01:00
selimaj-dev 66b96102f8 Working svg renderer 2026-02-22 16:41:26 +01:00
selimaj-dev 7fc05df240 Svg image rendering 2026-02-22 14:35:57 +01:00
selimaj-dev d32f124974 Removed todos 2026-02-22 04:27:30 +01:00
selimaj-dev fe4d999643 Merge pull request #23 from saturnclientmc/new-ui-theme-and-animations
New UI theme and animations
2026-02-21 18:48:24 -08:00
selimaj-dev cd84866ee8 Optional staggering 2026-02-22 03:44:22 +01:00
selimaj-dev cd5cac7f69 Shift menu animations 2026-02-22 03:36:39 +01:00
selimaj-dev 377031c0a9 Fixed click bugs 2026-02-22 03:20:16 +01:00
selimaj-dev 85d4a1de38 Animated mods 2026-02-22 02:44:58 +01:00
selimaj-dev 44d7aa3641 Hoo lee sheet 2026-02-22 02:31:56 +01:00
selimaj-dev 53732c5df3 Animation stagger and a few slide fixes 2026-02-22 02:19:04 +01:00
selimaj-dev 265b44187b Animation stagger delay 2026-02-22 01:38:30 +01:00
selimaj-dev f46f9c474e Animation Stagger element 2026-02-22 01:34:54 +01:00
selimaj-dev 049c92785d Fixed bugs and restructured roles 2026-02-22 01:25:22 +01:00
selimaj-dev 37ec729d1d Blurring 2026-02-22 01:11:12 +01:00
selimaj-dev 2747ad366e Fixed title menu freezing 2026-02-22 01:06:33 +01:00
selimaj-dev 7de3d5c9bc Fixed keybinding crashing when invalid 2026-02-22 00:20:12 +01:00
selimaj-dev f952fa7095 Better animation system 2026-02-22 00:12:13 +01:00
selimaj-dev e62da29a49 Sub namespace loading 2026-02-21 23:11:22 +01:00
selimaj-dev d3258edbc7 Cleaner Config 2026-02-21 22:55:47 +01:00
selimaj-dev 2096925a02 Smaller HexInput 2026-02-21 22:42:21 +01:00
selimaj-dev 3db44bab85 Fixed HexInput and stuff 2026-02-21 22:37:56 +01:00
selimaj-dev bba12146ea HexInput 2026-02-21 22:17:59 +01:00
selimaj-dev 4403a31c93 Fix splashoverlay and saturnmodule 2026-02-21 20:59:07 +01:00
selimaj-dev 6ee2fc3b0e Improved theme usability 2026-02-21 20:42:04 +01:00
selimaj-dev 3b0ab53d86 Fixed Theme issue 2026-02-21 20:35:52 +01:00
selimaj-dev 7683e7b91e Removed NamedProperty 2026-02-21 20:18:52 +01:00
selimaj-dev 3ba6202b43 Improved scroll 2026-02-21 20:09:19 +01:00
selimaj-dev a49212ca64 Attempting to fix scroll 2026-02-21 20:02:12 +01:00
selimaj-dev 88fe53b907 Fixed sidebar with abstractions 2026-02-21 19:57:40 +01:00
selimaj-dev 52f80281bd Fixed a few issues 2026-02-21 19:46:40 +01:00
selimaj-dev 2f93822996 Scrollbar 2026-02-21 19:38:19 +01:00
selimaj-dev 8d8cbc8b28 Implemenented theme 2026-02-21 19:29:36 +01:00
selimaj-dev a7c5f0d3df Fixed font stuff in screens 2026-02-21 19:19:54 +01:00
selimaj-dev 60d83530ae Tabmenu text and texture button 2026-02-21 19:18:42 +01:00
selimaj-dev 569aecde4a Scroll 2026-02-21 18:51:58 +01:00
selimaj-dev 9fd79aadd5 Notification 2026-02-21 18:47:19 +01:00
selimaj-dev e031672d72 Clicakble texture 2026-02-21 18:46:03 +01:00
selimaj-dev 289a2cd78a Button 2026-02-21 18:44:24 +01:00
selimaj-dev aab4f02dc0 Sidebar 2026-02-21 18:40:14 +01:00
selimaj-dev 5b37131bfd Saturn module 2026-02-21 18:34:12 +01:00
selimaj-dev 79ceaf67a1 Cosmetic Preview 2026-02-21 18:28:30 +01:00
selimaj-dev b0dbdeafa3 Toggle 2026-02-21 18:25:17 +01:00
selimaj-dev 3bc21a5b30 Keybinding selector and select 2026-02-21 18:19:44 +01:00
selimaj-dev 953182e56a Theme font, and implemented input 2026-02-21 18:13:36 +01:00
selimaj-dev 39cea14183 Theme padding and radius 2026-02-21 18:10:57 +01:00
selimaj-dev 8baf28fcfe Implemented theme 2026-02-21 18:05:26 +01:00
selimaj-dev 619c4bce21 Fixed role 2026-02-20 21:02:00 +01:00
selimaj-dev cc80fc5239 Merge pull request #21 from saturnclientmc/refactored-services-and-splash
Refactored services and splash
2026-02-20 11:56:48 -08:00
selimaj-dev f9039fdf67 Splash Background 2026-02-20 20:53:27 +01:00
selimaj-dev bb93d72f66 Progress bar 2026-02-20 20:12:17 +01:00
selimaj-dev 86be2eb956 Tested auth and fixed title menu 2026-02-20 19:44:43 +01:00
selimaj-dev eb40903372 SetScreen mixin 2026-02-20 18:11:58 +01:00
selimaj-dev 039d2cd032 SetScreen mixin 2026-02-20 17:35:59 +01:00
selimaj-dev 365063e8d5 Custom splash overlay 2026-02-20 16:41:09 +01:00
RudideC b43b076ca0 bugfix
you can now activate hitboxes without opening emotes
2026-02-20 11:10:00 +01:00
RudideC 71a7721c52 entity types 2026-02-20 10:12:19 +01:00
RudideC 2bcad83409 added nametags logo 2026-02-20 09:34:55 +01:00
selimaj-dev 6750abcc11 Service client refactor finalized 2026-02-20 03:47:32 +01:00
selimaj-dev 46d3408be9 TabList 2026-02-20 03:46:51 +01:00
selimaj-dev 8015a23342 Emote service 2026-02-20 03:42:56 +01:00
selimaj-dev 4e6bdce019 Buy cosmetics service 2026-02-20 03:41:17 +01:00
selimaj-dev 68519ac00c UI service auth refactored 2026-02-20 03:38:05 +01:00
selimaj-dev 50da953dc4 Added myself as the owner again 2026-02-20 03:25:33 +01:00
selimaj-dev f5776450f0 Name tag service 2026-02-20 03:24:09 +01:00
selimaj-dev 3b3283c6b0 Hat service 2026-02-20 03:11:08 +01:00
selimaj-dev 3f06efe8f4 Hat feature renderer 2026-02-20 03:09:50 +01:00
selimaj-dev 2d021e886f Cloaks 2026-02-20 03:07:31 +01:00
selimaj-dev 96f201cdb4 Auth 2026-02-20 02:45:42 +01:00
selimaj-dev 57c1d21407 Updated session version 2026-02-20 02:15:15 +01:00
selimaj-dev 5d8068c964 Before switching session versions 2026-02-20 01:44:03 +01:00
selimaj-dev 418c47f483 New structure 2026-02-20 00:31:19 +01:00
selimaj-dev 08c001afbb Session build 2026-02-19 23:21:03 +01:00
RudideC 052a2ac06c fixed small issue 2026-02-19 13:33:54 +01:00
RudideC 38041b01cf nametags module 2026-02-19 13:22:30 +01:00
Rudi fdda25464b Merge pull request #17 from RudideC/master
added github actions
2026-02-18 12:41:34 +01:00
RudideC 6bfdf66cdd push
will this work?
2026-02-18 12:37:03 +01:00
RudideC 82f8d0a550 maybe this will work?? 2026-02-18 12:21:17 +01:00
RudideC 81bfe41e25 no sources file 2026-02-18 12:06:58 +01:00
RudideC db0e627f5c fixed bug, dont upload sources jar 2026-02-18 10:13:04 +01:00
RudideC da970354e8 fix deprecation, simplified 2026-02-18 10:02:29 +01:00
RudideC 045e5cd5da test 2026-02-18 09:36:58 +01:00
RudideC e517377bdd ts 2026-02-18 09:25:09 +01:00
RudideC 520b5221b5 new version 2026-02-18 09:17:34 +01:00
selimaj-dev c567b282a5 Merge pull request #12 from RudideC/master
added health display
2026-02-13 12:03:56 +01:00
RudideC 5f19abebbf added health display
ts tuff
2026-02-13 10:06:13 +01:00
Klesti Selimaj 58eda56f37 Merge pull request #11 from RudideC/master
zoom mod
2026-01-21 21:57:42 +01:00
RudideC 50cab402d4 image 2026-01-21 21:52:28 +01:00
RudideC fa7161d371 zoom module 2026-01-21 21:52:24 +01:00
Klesti Selimaj 252808a467 Merge pull request #10 from RudideC/master
added nofog, daycounter and speedometer
2026-01-20 21:56:36 +01:00
RudideC 31e9a8d56e speedometer module finished
logo included
2026-01-20 15:30:57 +01:00
RudideC ba1b68d8fa speedometer works in vehicles 2026-01-20 14:57:59 +01:00
RudideC dcd82fa58e default is x axis 2026-01-20 14:51:22 +01:00
RudideC 4132a05c8f blocks/tick -> blocks/s 2026-01-20 14:44:29 +01:00
RudideC 47f8dd5e22 testing speedometer 2026-01-20 14:35:49 +01:00
RudideC 9b30ec7016 small change 2026-01-20 14:18:44 +01:00
RudideC d10f229ffa daycounter image 2026-01-19 20:55:07 +01:00
RudideC 061ed674e3 daycounter mod
counts days
2026-01-19 20:54:59 +01:00
RudideC 5b5f781095 nofog mod
you can customise if you want to be able to see through liquids, but only lava and powdered snow work (not water)
2026-01-17 00:24:39 +01:00
Klesti Selimaj 48f7aefbf6 Merge pull request #9 from RudideC/master 2026-01-16 22:12:15 +01:00
RudideC 8f111badf6 oops, forgot 2026-01-16 22:05:35 +01:00
RudideC 687bda68a1 renamed potiondisplay to status effects
this is the name most clients use
2026-01-16 22:04:57 +01:00
RudideC 8e2c986ae4 fullbright
new mod
2026-01-16 22:02:48 +01:00
RudideC faa95e664a faster timeout
(so i don't have to wait minutes before testing)
2026-01-16 22:02:25 +01:00
RudideC c9230da168 faster builds
yes
2026-01-16 22:01:59 +01:00
RudideC 7002fbec0e prevent race condition 2026-01-16 19:41:37 +01:00
RudideC 496d27b861 enabled mod list updates
each mod enable/disable updates the list
2026-01-16 19:41:29 +01:00
RudideC 3c37b80bc2 dont sprint when using item
anticheat compliance
2026-01-16 19:02:51 +01:00
selimaj-dev 920657ddd3 Beta 2 2025-09-08 18:46:29 +02:00
selimaj-dev 32035a2f7d Fixed flat cloak with new optimised textures 2025-09-06 12:07:30 +02:00
Leo Codes c230f4f5ec Merge pull request #7 from saturnclientmc/optimised-cloak
Optimised cloak texture size
2025-09-06 02:24:46 +02:00
selimaj-dev 50d1028e53 Optimised cloak texture size 2025-09-06 02:21:40 +02:00
Leo Codes 780cb9ba9a Merge pull request #6 from saturnclientmc/cloak-edge-cases
Cloak edge cases
2025-09-05 18:05:24 +02:00
selimaj-dev 65df9e9a5e Fixed cloak when crouching #5 2025-09-05 17:49:45 +02:00
selimaj-dev 66f8a13faa Added a wave effect #5 2025-09-05 17:09:46 +02:00
selimaj-dev 1726542c84 Fixed cloak when swimming #5 2025-09-05 16:35:57 +02:00
selimaj-dev 71f6597624 0.0.5 beta 1 and a few changes to the cloak 2025-09-04 20:04:38 +02:00
selimaj-dev 45f8c65549 Bendy cloak option and slight cloak jiggle 2025-09-04 19:54:35 +02:00
selimaj-dev 82bb2859c9 Fixed store purchase 2025-09-04 19:30:25 +02:00
Leo Codes 4d87c4e694 Merge pull request #4 from saturnclientmc/updated-cloak
Optimised, added wavy cloaks and added new cloaks
2025-09-04 18:19:49 +02:00
selimaj-dev 5c7283e513 Wavey cloaks 2025-09-04 17:28:17 +02:00
selimaj-dev ee300b2bb2 Wavey cloaks (no bottom) 2025-09-04 16:35:26 +02:00
selimaj-dev 3b1f54a5b0 Okay getting somewhere 2025-09-04 15:57:36 +02:00
selimaj-dev e39c877b91 All sides except top and bottom 2025-09-04 15:43:40 +02:00
selimaj-dev 4bb4e46487 Testing for curved cloak 2025-09-04 15:15:34 +02:00
selimaj-dev d56bdfea8e Working custom renderer for cloaks 2025-09-04 11:41:57 +02:00
selimaj-dev afca9c01b7 Working custom renderer for cloaks 2025-09-04 03:26:37 +02:00
selimaj-dev 40b0177cf2 Working custom renderer for cloaks 2025-09-03 17:50:57 +02:00
selimaj-dev db27de497a Slightly better light for cloaks 2025-09-03 08:20:18 +02:00
selimaj-dev 53fd88df18 Optimised cloak loading 2025-09-03 08:01:32 +02:00
selimaj-dev 8e477f8852 Main hand on armor status 2025-09-03 05:28:41 +02:00
selimaj-dev 1f93f6cf5a Fixed red bull cloak 2025-09-03 05:11:14 +02:00
selimaj-dev a4c60dd5b0 Fixed cloak store 2025-09-03 05:10:00 +02:00
selimaj-dev 72766a47ac Added cloaks to list 2025-09-03 04:45:40 +02:00
selimaj-dev 4b7238e863 Black hole cloak previews 2025-09-03 04:28:19 +02:00
selimaj-dev 52456f2ca8 Black hole cloaks 2025-09-03 04:26:03 +02:00
selimaj-dev a464635075 Cloak fix 2025-09-03 02:57:41 +02:00
selimaj-dev 8089deb172 Optimisations 2025-09-03 02:51:19 +02:00
selimaj-dev ca9626f340 Testing for cloak 2025-09-03 01:34:22 +02:00
selimaj-dev 0d3a27b5e7 Beta 2 2025-08-22 17:36:44 +02:00
selimaj-dev b6eca22097 Fixed alignment in bmw and amg cloaks 2025-08-21 01:49:58 +02:00
selimaj-dev 4de4605ee8 Red Bull cloak (sick af) 2025-08-21 01:44:45 +02:00
selimaj-dev 8018caf26b Ferrari 2025-08-21 01:22:03 +02:00
selimaj-dev 39266df45e Amg petronas f1 cloak 2025-08-20 18:14:45 +02:00
selimaj-dev 25046b53f3 Amg cloak 2025-08-20 17:26:11 +02:00
selimaj-dev 8e89a850b3 Bmw preview 2025-08-19 20:27:55 +02:00
selimaj-dev ccaa79e89b Bmw cloak 2025-08-19 20:24:23 +02:00
selimaj-dev 30b375aa6f Bmw cloak 2025-08-19 20:16:39 +02:00
selimaj-dev 6d2cad1c7a Bmw cloak 2025-08-19 20:16:32 +02:00
selimaj-dev 720ee3d8bd change version to 0.0.4-beta1 2025-08-18 14:00:40 +02:00
selimaj-dev b4b348a54c change version to 0.0.2-beta1 2025-08-18 14:00:23 +02:00
selimaj-dev a3be72c443 Loading in theme manager with refactor 2025-08-18 11:58:14 +02:00
selimaj-dev 1c553d14bf Fixed theme manager bug 2025-08-18 09:25:38 +02:00
selimaj-dev cd967a907c Changed emote icons 2025-08-17 20:34:40 +02:00
selimaj-dev ba04c45b75 readme change 2025-08-17 15:26:45 +02:00
selimaj-dev ecb07ce730 readme change 2025-08-17 15:25:41 +02:00
selimaj-dev 6cd10240fc Merge branch 'refactored-clientcom' 2025-08-17 15:23:24 +02:00
selimaj-dev d162db3464 Fixed keypress and hud rendering 2025-08-17 15:22:00 +02:00
Leo Codes 7e3b6cf7f8 Merge pull request #3 from saturnclientmc/refactored-clientcom
Refactored clientcom
2025-08-16 10:39:06 -04:00
selimaj-dev 6fb38e4f9b Updated ip 2025-08-16 16:33:19 +02:00
selimaj-dev 997fbbe9a9 Working emote system 2025-08-16 16:29:48 +02:00
selimaj-dev 01a8ea15a0 Better cosmetic update system 2025-08-16 16:05:44 +02:00
selimaj-dev 08cee2172c Removed the reload system 2025-08-16 14:17:55 +02:00
selimaj-dev 2c64eed329 new version 2025-07-07 01:01:08 +02:00
selimaj-dev 66d7d7eab9 Disabled online emotes 2025-07-07 00:54:56 +02:00
selimaj-dev 21b1dcc997 saturn client branding 2025-07-05 05:20:34 +02:00
selimaj-dev c2470378e1 saturn client branding 2025-07-05 05:20:27 +02:00
selimaj-dev f57e773a44 fixed hat still visible when invisible 2025-07-05 04:02:30 +02:00
selimaj-dev 6c0575a03f Notifications 2025-06-23 17:03:18 -05:00
selimaj-dev 9f93ea923e Cosmetic preview slight improvement 2025-06-23 17:16:28 +02:00
selimaj-dev ecf5987434 sidebar gap 2025-06-23 17:04:03 +02:00
selimaj-dev 89a5b2437e fully functional store 2025-06-23 16:49:09 +02:00
selimaj-dev c8d28133a0 Crimson Mark 2025-06-23 07:12:54 +02:00
selimaj-dev 0980e3a5d4 semi working Store 2025-06-22 16:56:18 -05:00
selimaj-dev 420ba82b44 more emotes 2025-06-22 11:24:38 -05:00
selimaj-dev 2f3c125bba Keystrokes mouse and space options 2025-06-19 15:13:29 -05:00
selimaj-dev 9e64b2332e added keystrokes 2025-06-18 13:47:50 -05:00
selimaj-dev 089cf4778c freelook 2025-06-18 08:51:14 -05:00
selimaj-dev b9984c35a2 coord display methods 2025-06-18 05:10:15 -05:00
selimaj-dev b035381ba7 removed unnecessary resources 2025-06-18 04:30:17 -05:00
selimaj-dev 87b326ce9f coords 2025-06-18 04:26:39 -05:00
selimaj-dev 786f9cf6f7 small change 2025-06-18 00:29:57 +02:00
selimaj-dev 464d0b502b NONE key selection when backspace 2025-06-17 13:13:56 -05:00
selimaj-dev e38f4480a3 config save bug fix 2025-06-17 11:54:50 -05:00
selimaj-dev d178f1504b config for keybidnings 2025-06-17 11:42:19 -05:00
selimaj-dev 5b77ea7363 Small property refactor 2025-06-17 10:19:49 -05:00
selimaj-dev 78097b1d8f prep for module mixins 2025-06-17 09:54:03 -05:00
selimaj-dev 51040900cf potion display 2025-06-16 16:12:40 -05:00
selimaj-dev f06e2a0aaa updated icon 2025-06-16 16:06:19 -05:00
selimaj-dev 70fcd64f25 potion display (needs dummy and proper dimensions) 2025-06-16 11:49:56 -05:00
selimaj-dev b2c7e9f03e default transparent hud mod background 2025-06-16 07:31:56 -05:00
selimaj-dev 08155bd49a fixed armor display 2025-06-16 07:31:14 -05:00
selimaj-dev 67a5b219f5 better font management 2025-06-16 07:06:15 -05:00
selimaj-dev 6c608409e0 Added emote cancel (shift) 2025-06-15 09:53:50 -05:00
selimaj-dev 4c72f8e39f online emotes (todo: make the emotes cancel across the players) 2025-06-15 01:32:28 +02:00
selimaj-dev 3d9d01a28f fps display 2025-06-14 14:09:27 -05:00
selimaj-dev 7e39532d1d almost fully working emotes 2025-06-14 13:52:29 -05:00
selimaj-dev 76c20d8733 emotes (real) 2025-06-14 06:14:25 +02:00
selimaj-dev a1c68609d9 working emote test 2025-06-14 04:21:34 +02:00
selimaj-dev d49afbb981 updated mercedes flow 2025-05-18 23:03:16 +02:00
selimaj-dev fd0bf75750 beta2 2025-05-17 15:31:22 +02:00
selimaj-dev edbfd0306a added mod detials 2025-05-17 01:56:26 +02:00
selimaj-dev 324ffe0c8b some ui stuff 2025-05-17 01:24:27 +02:00
selimaj-dev 21daf473b1 added armor display 2025-05-16 07:34:40 -05:00
Leo Codes d1030bb235 Merge pull request #2 from saturnclientmc/refactor
Refactor
2025-05-13 02:06:52 -05:00
selimaj-dev 05fcd3032b default colors changed 2025-05-13 00:03:09 +02:00
selimaj-dev 47cafad92b cosmetic skin fix? 2025-05-12 23:40:34 +02:00
selimaj-dev 744698fd71 bye bye old ui 2025-05-12 23:07:06 +02:00
selimaj-dev 86b638afca few changes to finish today 2025-05-12 15:04:08 -05:00
selimaj-dev a016fd72c6 added the settings menu for ui 2 2025-05-12 14:39:03 -05:00
selimaj-dev 8e8737afcd added a toggle 2025-05-12 11:19:26 -05:00
selimaj-dev 6eb3fc8092 color picker 2025-05-12 10:28:37 -05:00
selimaj-dev f9baa0fe5f added FloatInput 2025-05-12 07:18:32 -05:00
selimaj-dev a6e762b0d8 Keypress implementation on ui2 2025-05-12 07:03:32 -05:00
selimaj-dev d72b152a26 ok 2025-05-11 05:00:18 -05:00
selimaj-dev 2a202c4160 added cosmetic menus 2025-05-11 04:36:14 -05:00
selimaj-dev 930d002b81 this time it should be good (aside from colors) 2025-05-11 02:53:31 -05:00
selimaj-dev 35440180ea made the mod actually good 2025-05-09 01:54:17 +02:00
selimaj-dev 7fd0565f7f better mod card design 2025-05-08 09:54:15 -05:00
selimaj-dev 1421d94647 a lot of stuff idk 2025-05-07 12:36:23 -05:00
selimaj-dev fbb1460872 slighly better font 2025-05-07 12:20:43 -05:00
selimaj-dev e7ca5c0297 slighly better font 2025-05-07 11:32:16 -05:00
selimaj-dev 3a54739410 slighly better font 2025-05-07 10:07:57 -05:00
selimaj-dev 1c80803994 title screen change 2025-05-07 12:41:01 +02:00
selimaj-dev 191e9eba4c column fix 2025-05-07 03:05:16 +02:00
selimaj-dev 3d654685d0 small ui changes 2025-05-07 00:58:38 +02:00
selimaj-dev d9bd2c60c9 logo update 2025-05-07 00:26:45 +02:00
selimaj-dev 984132991d Better module display 2025-05-06 23:46:26 +02:00
selimaj-dev 7d649efa22 Themified mods and state merging 2025-05-06 20:48:23 +02:00
selimaj-dev 1ffadc9b45 font 2025-05-06 08:00:21 -05:00
selimaj-dev 9a7b93ccbe mod ui 2025-05-06 08:00:09 -05:00
selimaj-dev d37d4e49f9 New mod ui 2025-05-06 07:23:07 -05:00
selimaj-dev b491f246c8 Dupe fix 2025-05-06 06:31:50 -05:00
selimaj-dev cfe518380a Theming 2025-05-06 06:27:42 -05:00
selimaj-dev fe2521bee2 height fix on scroll 2025-05-06 05:02:11 -05:00
selimaj-dev ae4d9786b2 functional scrolling and functional mod menu 2025-05-06 05:37:06 +02:00
selimaj-dev 0f4af22346 scrolling 2025-05-06 03:18:56 +02:00
selimaj-dev 5a8ae263e9 scissor stack 2025-05-06 01:51:32 +02:00
selimaj-dev 2458906e3b Merge branch 'refactor' of github.com:saturnclientmc/saturnclient into refactor 2025-05-05 11:21:04 -05:00
selimaj-dev 42611d4aed added scrolling (need to add scissor) 2025-05-05 11:20:17 -05:00
selimaj-dev e5cc68ecad updated horns model 2025-05-04 00:58:52 +02:00
selimaj-dev b2ba693f92 alpha2 2025-05-03 22:36:48 +02:00
selimaj-dev cf9fd50e4b slight title menu changes 2025-05-03 13:53:42 -05:00
selimaj-dev f1f17d807f updated the title screen with the new ui 2025-05-03 11:32:41 -05:00
selimaj-dev 22ad088ca4 fixed panorama 2025-05-03 11:19:33 -05:00
selimaj-dev 52e8e572be fixed panorama 2025-05-03 10:15:52 -05:00
selimaj-dev a9ec3311c5 added sprint 2025-05-01 18:10:44 +02:00
selimaj-dev be1cca9c9a alpha blending 2025-05-01 09:33:13 -05:00
selimaj-dev 84f76ac577 lowered the blur 2025-05-01 09:26:51 -05:00
selimaj-dev 76b882599b blur and animation abstraction 2025-05-01 16:21:27 +02:00
selimaj-dev 5b6810a32f removed borders 2025-04-30 07:52:30 -05:00
selimaj-dev f0e554d8bc double the resolution 2025-04-29 05:55:09 -05:00
selimaj-dev bb9cc623c7 added animations 2025-04-29 05:42:25 -05:00
selimaj-dev bf4e16d940 added animations 2025-04-29 05:40:10 -05:00
selimaj-dev 9849a38fe9 working animations 2025-04-29 01:10:23 +02:00
selimaj-dev d16b66945f working animations 2025-04-29 01:10:10 +02:00
selimaj-dev c0b088bb8a experiment: animation curve functions 2025-04-28 15:20:16 -05:00
selimaj-dev 5cce4eabaf new ui elements 2025-04-27 10:45:45 -05:00
selimaj-dev 1254cd4b89 few theme stuff to button and rounded corners 2025-04-26 15:43:10 +02:00
selimaj-dev ff02427aa3 Rounded borders 2025-04-26 14:55:13 +02:00
selimaj-dev 79d5419fcc Fixed crash when exit 2025-04-26 00:23:39 +02:00
selimaj-dev ece5a16d34 Fixed crash when exit 2025-04-26 00:10:39 +02:00
selimaj-dev 32c1391336 Fixed crash when exit 2025-04-25 23:52:37 +02:00
264 changed files with 17071 additions and 5870 deletions
+45
View File
@@ -0,0 +1,45 @@
name: Automate Gradlew Build
on:
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
check-latest: true
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-home-cache-cleanup: false
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build (release)
run: ./gradlew clean build -x test --no-daemon --stacktrace
- name: Remove sources JARs
run: |
rm -f build/libs/*sources*.jar
echo "Removed sources JARs from build/libs"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: saturnclient-latest
path: build/libs/saturnclient-*.jar
if-no-files-found: error
+52
View File
@@ -0,0 +1,52 @@
name: Push Builds
on:
push:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-home-cache-cleanup: false
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build
run: ./gradlew clean build -x test --no-daemon --stacktrace
- name: Remove sources JARs
run: |
rm -f build/libs/*sources*.jar
echo "Removed sources JARs from build/libs"
- name: Create timestamped JAR
run: |
TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S")
MAIN_JAR=$(ls build/libs/saturnclient-*.jar | head -1)
cp "$MAIN_JAR" "saturnclient-${TIMESTAMP}.jar"
echo "Created: saturnclient-${TIMESTAMP}.jar"
- name: Upload timestamped artifact
uses: actions/upload-artifact@v4
with:
name: saturnclient-${{ github.run_number }}
path: saturnclient-*.jar
if-no-files-found: error
+3 -1
View File
@@ -8,10 +8,12 @@
<b>Saturn Client is a free and open source Minecraft client</b>
</p>
![image](https://github.com/user-attachments/assets/b0b23e72-c2eb-49aa-b49c-4d3cc4d60fcc)
# Credits
- Cloak rendering: `IIpho3nix` & `Dhyan Blum`
- Who Am I: `FokshaWasTaken`
- Panton font (ui): `Ivan Petrov` & `Fontfabric`
- Panorama: https://www.planetminecraft.com/texture-pack/minecraft-cavesand-cliffs-part-ii-main-menu-panorama/
If any of the code or assets are in your name and you want to remove them, please submit an issue, we are still in alpha to have any artists so we would also appreciate if you could help us make some cosmetics
+138 -25
View File
@@ -1,35 +1,126 @@
/*
* ================================================================
* Saturn Client - Fabric Mod Build Configuration
* ================================================================
*
* This build file configures:
* - Fabric Loom (Minecraft mod development)
* - Dependency management
* - Resource processing
* - Java toolchain (Java 21)
* - Publishing setup
*
* All version values are defined in gradle.properties.
* ================================================================
*/
plugins {
// Fabric mod development plugin
id 'fabric-loom' version '1.9-SNAPSHOT'
// Maven publishing support
id 'maven-publish'
}
/*
* ================================================================
* Project Metadata
* ================================================================
*/
version = project.mod_version
group = project.maven_group
group = project.maven_group
repositories {}
/*
* ================================================================
* Repositories
* ================================================================
* All dependency sources.
*/
repositories {
// Fabric
maven { url 'https://maven.fabricmc.net/' }
// JitPack (GitHub releases)
maven { url 'https://jitpack.io' }
// Parchment mappings (if needed later)
maven { url 'https://maven.parchmentmc.org' }
// Special Model Loader
maven { name = "FelNull Maven"; url = "https://maven.felnull.dev" }
// Player Animation Library
maven { name = "KosmX Maven"; url = 'https://maven.kosmx.dev/' }
// Central repository
mavenCentral()
}
/*
* ================================================================
* Dependencies
* ================================================================
*/
dependencies {
// ─────────────────────────────────────────────
// Minecraft Core
// ─────────────────────────────────────────────
minecraft "com.mojang:minecraft:${project.minecraft_version}"
// Switch back to Yarn
// Yarn mappings (named mappings for dev environment)
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
// Fabric Loader
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
// ─────────────────────────────────────────────
// Rendering / Model Libraries
// ─────────────────────────────────────────────
// Special Model Loader (included inside your mod jar)
modApi "dev.felnull:special-model-loader:1.4.0+pre.1"
include "dev.felnull:special-model-loader:1.4.0+pre.1"
// SVG Rendering Library
include implementation("com.formdev:svgSalamander:1.1.4")
// ─────────────────────────────────────────────
// Animation
// ─────────────────────────────────────────────
// Player Animation Library (bundled into your mod)
include modImplementation(
"dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}"
)
// Optional: BendyLib (only if you directly use it)
// include modRuntimeOnly(
// "io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}"
// )
// ─────────────────────────────────────────────
// External Libraries
// ─────────────────────────────────────────────
// Session Java (auth/session handling)
include implementation("com.github.selimaj-dev:session-java:0.1.6")
}
repositories {
maven { url "https://maven.parchmentmc.org" }
maven {
name = "FelNull Maven"
url = "https://maven.felnull.dev"
}
}
/*
* ================================================================
* Resource Processing
* ================================================================
* Injects version values into fabric.mod.json.
*/
processResources {
inputs.property "version", project.version
inputs.property "minecraft_version", project.minecraft_version
@@ -37,38 +128,58 @@ processResources {
filteringCharset "UTF-8"
filesMatching("fabric.mod.json") {
expand "version": project.version,
expand(
"version": project.version,
"minecraft_version": project.minecraft_version,
"loader_version": project.loader_version
)
}
}
/*
* ================================================================
* Java Configuration
* ================================================================
* Targeting Java 21.
*/
def targetJavaVersion = 21
tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8"
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
it.options.release.set(targetJavaVersion)
}
options.encoding = "UTF-8"
options.release.set(targetJavaVersion)
}
java {
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
if (JavaVersion.current() < javaVersion) {
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
}
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
archivesBaseName = project.archives_base_name
withSourcesJar()
}
/*
* ================================================================
* Jar Packaging
* ================================================================
* Includes LICENSE and NOTICE inside the final jar.
*/
jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"}
rename { "${it}_${project.archivesBaseName}" }
}
from("NOTICE") {
rename { "${it}_${project.archivesBaseName}"}
rename { "${it}_${project.archivesBaseName}" }
}
}
/*
* ================================================================
* Publishing
* ================================================================
* Configured but no remote repository defined yet.
*/
publishing {
publications {
mavenJava(MavenPublication) {
@@ -76,5 +187,7 @@ publishing {
}
}
repositories {}
repositories {
// Add publishing repository here when needed
}
}
File diff suppressed because one or more lines are too long
+14 -13
View File
@@ -1,17 +1,18 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
org.gradle.jvmargs=-Xmx1G -XX:+UseParallelGC
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.7
loader_version=0.16.10
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true
# Mod Properties
mod_version = 0.0.1-alpha1
maven_group = me.iipho3nix
archives_base_name = saturnclient
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.7
loader_version=0.16.10
fabric_version=0.114.0+1.21.4
# Dependencies
# check this on https://modmuss50.me/fabric.html
fabric_version=0.114.0+1.21.4
mod_version = 0.0.7-beta1
maven_group = org.saturnclient
archives_base_name = saturnclient
player_anim = 2.0.3+1.21.4
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

@@ -1,11 +1,11 @@
package org.saturnclient.modules;
import net.minecraft.client.gui.DrawContext;
import org.saturnclient.ui2.RenderScope;
public interface HudMod {
public ModDimensions getDimensions();
public void renderDummy(DrawContext context);
public void renderDummy(RenderScope scope);
public void render(DrawContext context);
public void renderHud(RenderScope scope);
}
@@ -1,64 +1,54 @@
package org.saturnclient.modules;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import java.util.Map;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.saturnclient.config.manager.Property.NamedProperty;
public class ModDimensions {
public Property<Integer> x = new Property<>(0);
public Property<Integer> y = new Property<>(0);
public Property<Float> scale = new Property<>(1.0f);
public Property<Integer> x = Property.integer(0);
public Property<Integer> y = Property.integer(0);
public Property<Float> scale = Property.floatProp(1.0f);
public Property<Integer> bgColor = Property.color(0x00000000);
public Property<Integer> fgColor = Property.color(0xFFffffff);
public Property<Integer> radius = Property.integer(0);
public Property<Integer> font = Property.font(0);
public boolean renderBackground = true;
public int width = 0;
public int height = 0;
private final ConfigManager dimensionsConfig;
public ModDimensions(ConfigManager configManager) {
dimensionsConfig = new ConfigManager(configManager, "Dimensions");
dimensionsConfig.property("x", this.x);
dimensionsConfig.property("y", this.y);
dimensionsConfig.property("scale", this.scale);
public NamedProperty<Map<String, Property<?>>> prop() {
return Property.namespace(Map.of(
"X", x,
"Y", y,
"Scale", scale,
"Background Color", bgColor,
"Foreground Color", fgColor,
"Corner Radius", radius,
"Font", font)).named("In-Game Display");
}
public ModDimensions(ConfigManager configManager, int x, int y) {
dimensionsConfig = new ConfigManager(configManager, "Dimensions");
this.x.value = x;
this.y.value = y;
dimensionsConfig.property("x", this.x);
dimensionsConfig.property("y", this.y);
dimensionsConfig.property("scale", this.scale);
public ModDimensions() {
}
public ModDimensions(ConfigManager configManager, int x, int y, int width, int height) {
dimensionsConfig = new ConfigManager(configManager, "Dimensions");
public ModDimensions(int width, int height) {
this.width = width;
this.height = height;
}
public ModDimensions(int x, int y, int width, int height) {
this.x.value = x;
this.y.value = y;
this.width = width;
this.height = height;
dimensionsConfig.property("x", this.x);
dimensionsConfig.property("y", this.y);
dimensionsConfig.property("scale", this.scale);
}
public ModDimensions(ConfigManager configManager, int x, int y, float scale) {
dimensionsConfig = new ConfigManager(configManager, "Dimensions");
this.x.value = x;
this.y.value = y;
this.scale.value = scale;
dimensionsConfig.property("x", this.x);
dimensionsConfig.property("y", this.y);
dimensionsConfig.property("scale", this.scale);
}
public ModDimensions(ConfigManager configManager, int x, int y, int width, int height, float scale) {
dimensionsConfig = new ConfigManager(configManager, "Dimensions");
public ModDimensions(int x, int y, int width, int height, float scale) {
this.x.value = x;
this.y.value = y;
this.scale.value = scale;
this.width = width;
this.height = height;
dimensionsConfig.property("x", this.x);
dimensionsConfig.property("y", this.y);
dimensionsConfig.property("scale", this.scale);
}
}
@@ -1,57 +1,60 @@
package org.saturnclient.modules;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.util.math.MatrixStack;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.modules.mods.*;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
public class ModManager {
public static Module[] MODS = {
new Crosshair()
public static final List<Module> ENABLED_MODS = new ArrayList<>();
public static final Module[] ALL_MODS = {
new Crosshair(),
new AutoSprint(),
new ArmorDisplay(),
new Fps(),
new StatusEffects(),
new Coordinates(),
new Freelook(),
new Keystrokes(),
new Fullbright(),
new NoFog(),
new DayCounter(),
new Speedometer(),
new Zoom(),
new HealthDisplay(),
new Nametags(),
new Ping(),
new Tps(),
new Clock()
};
public static void init() {
// Pre-filter enabled modules to avoid checking every tick
updateEnabledModules();
ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (Module m : MODS) {
if (m.isEnabled()) {
m.tick();
}
}
});
HudRenderCallback.EVENT.register((context, _o) -> {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
if (textRenderer != null &&
!(SaturnClient.client.currentScreen instanceof HudEditor)) {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (Module m : MODS) {
if (m instanceof HudMod && m.isEnabled()) {
ModDimensions dim = ((HudMod) m).getDimensions();
MatrixStack matrices = context.getMatrices();
matrices.push();
matrices.translate(dim.x.value, (float) dim.y.value, 0);
matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
((HudMod) m).render(context);
matrices.pop();
} else {
m.render(renderScope);
}
}
// Only iterate over enabled modules
for (Module m : ENABLED_MODS) {
m.tick();
}
});
}
public static synchronized void updateEnabledModules() {
ENABLED_MODS.clear();
for (Module m : ALL_MODS) {
if (m.isEnabled()) {
ENABLED_MODS.add(m);
}
}
}
public static Module[] getAllMods() {
return ALL_MODS;
}
public static void refreshEnabledModules() {
updateEnabledModules();
}
}
@@ -1,51 +1,60 @@
package org.saturnclient.modules;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.NamedProperty;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.config.manager.Property.NamedProperty;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.util.Identifier;
public abstract class Module {
private final String name;
private final String namespace;
private final ModuleDetails details;
private final ConfigManager configManager;
public Module(String name, String namespace, NamedProperty<?>... props) {
this.name = name;
this.namespace = namespace;
configManager = new ConfigManager(name);
public Module(ModuleDetails details, NamedProperty<?>... props) {
this.details = details;
configManager = new ConfigManager(details.name);
for (NamedProperty<?> prop : props) {
configManager.property(prop.name, prop.prop);
}
}
public Module(ConfigManager config, String name, String namespace) {
this.name = name;
this.namespace = namespace;
configManager = config;
public void render(RenderScope scope) {
}
public void render(RenderScope scope) {}
public void tick() {}
public void tick() {
}
public abstract boolean isEnabled();
public abstract void setEnabled(boolean e);
public final void setEnabled(boolean e) {
onEnabled(e);
ModManager.updateEnabledModules();
}
public abstract void onEnabled(boolean e);
public final String getName() {
return name;
return details.name;
}
public Identifier getIconTexture() {
return Textures.getModIcon(namespace);
return Textures.getModIcon(details.namespace);
}
public ConfigManager getConfig() {
return configManager;
}
public String getDescription() {
return details.description;
}
public String[] getTags() {
return details.tags;
}
public String getVersion() {
return details.version;
}
}
@@ -0,0 +1,39 @@
package org.saturnclient.modules;
public class ModuleDetails {
public String name;
public String namespace;
public String description;
public String version;
public String[] tags = {};
public ModuleDetails(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
public ModuleDetails name(String name) {
this.name = name;
return this;
}
public ModuleDetails namespace(String namespace) {
this.namespace = namespace;
return this;
}
public ModuleDetails description(String description) {
this.description = description;
return this;
}
public ModuleDetails version(String version) {
this.version = version;
return this;
}
public ModuleDetails tags(String... tags) {
this.tags = tags;
return this;
}
}
@@ -0,0 +1,6 @@
# Credits
### Freelook
- https://github.com/Celibistrial/freelook
- https://modrinth.com/mod/freelook
**The other mods are coded by us, if you have a concern or we missed something feel free to submit a issue with the credits tag**
@@ -0,0 +1,102 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
public class ArmorDisplay extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 75);
private static Property<Boolean> useMainHand = Property.bool(false);
public ArmorDisplay() {
super(new ModuleDetails("Armor Display", "armor")
.description("Displays armor durability")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
useMainHand.named("Use Main Hand"),
dimensions.prop());
}
public void renderArmor(RenderScope scope, ItemStack mainHand, ItemStack helmet, ItemStack chestplate,
ItemStack leggings, ItemStack boots) {
int row = 0;
if (useMainHand.value && !mainHand.isEmpty()) {
scope.drawItem(mainHand, 0, 15 * row);
renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
row++;
}
if (!helmet.isEmpty()) {
scope.drawItem(helmet, 0, 15 * row);
renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
row++;
}
if (!chestplate.isEmpty()) {
scope.drawItem(chestplate, 0, 15 * row);
renderHealth(scope, row, chestplate.getMaxDamage(), chestplate.getDamage());
row++;
}
if (!leggings.isEmpty()) {
scope.drawItem(leggings, 0, 15 * row);
renderHealth(scope, row, leggings.getMaxDamage(), leggings.getDamage());
row++;
}
if (!boots.isEmpty()) {
scope.drawItem(boots, 0, 15 * row);
renderHealth(scope, row, boots.getMaxDamage(), boots.getDamage());
row++;
}
}
public void renderHealth(RenderScope scope, int i, int max_damage, int damage) {
if (max_damage > 0) {
scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value,
dimensions.fgColor.value);
}
}
@Override
public void renderHud(RenderScope scope) {
renderArmor(scope,
SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET));
}
@Override
public void renderDummy(RenderScope scope) {
renderArmor(scope, new ItemStack(Items.DIAMOND_SWORD), new ItemStack(Items.DIAMOND_HELMET),
new ItemStack(Items.DIAMOND_CHESTPLATE),
new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS));
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,46 @@
package org.saturnclient.modules.mods;
import java.util.Objects;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
public class AutoSprint extends Module {
private static Property<Boolean> enabled = Property.bool(false);
public AutoSprint() {
super(
new ModuleDetails("Auto Sprint", "sprint")
.description("Makes the player always sprint")
.tags("Movement")
.version("v0.1.0"),
enabled.named("Enabled"));
}
@Override
public void tick() {
if (SaturnClient.client.player == null || SaturnClient.client.getNetworkHandler() == null) {
return;
}
if (SaturnClient.client.options.forwardKey.isPressed()
&& !SaturnClient.client.options.backKey.isPressed()
&& !SaturnClient.client.player.isSneaking()
&& !SaturnClient.client.player.horizontalCollision
&& !SaturnClient.client.player.isUsingItem()) {
Objects.requireNonNull(SaturnClient.client.player).setSprinting(true);
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,77 @@
package org.saturnclient.modules.mods;
import java.time.format.DateTimeFormatter;
import java.time.LocalTime;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class Clock extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> format = Property.select(0, "24 hour", "12 hour");
private static Property<Boolean> seconds = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
public Clock() {
super(new ModuleDetails("Clock", "clock")
.description("Displays the current real-world time")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
format.named("Format"),
seconds.named("Show seconds"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderClock(scope);
}
@Override
public void renderHud(RenderScope scope) {
renderClock(scope);
}
public void renderClock(RenderScope scope) {
String text = "";
LocalTime currentTime = LocalTime.now();
switch (format.value) {
case 0:
if (seconds.value) { text = currentTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")); }
else { text = currentTime.format(DateTimeFormatter.ofPattern("HH:mm")); }
break;
case 1:
if (seconds.value) { text = currentTime.format(DateTimeFormatter.ofPattern("hh:mm:ss a")); }
else { text = currentTime.format(DateTimeFormatter.ofPattern("hh:mm a")); }
break;
}
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,75 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
public class Coordinates extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMethod = Property.select(0, "Flat", "Flat annotated", "Newline", "Newline annotated");
private static ModDimensions dimensions = new ModDimensions(40, 18);
public Coordinates() {
super(new ModuleDetails("Coordinates", "coords")
.description("Displays your current coordinates")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMethod.named("Display Method"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderCoords(532, 69, 253, scope);
}
@Override
public void renderHud(RenderScope scope) {
Vec3d pos = SaturnClient.client.player.getPos();
renderCoords((int) pos.x, (int) pos.y, (int) pos.z, scope);
}
public void renderCoords(int x, int y, int z, RenderScope scope) {
String f = "%d %d %d"; // Flat
switch (displayMethod.value) {
case 1: // Flat annotated
f = "X: %d Y: %d Z: %d";
break;
case 2: // Newline
f = "%d\n%d\n%d";
break;
case 3: // Newline annotated
f = "X: %d\nY: %d\nZ: %d";
break;
}
String text = String.format(f, x, y, z);
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,34 +1,38 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Textures;
import net.minecraft.client.render.RenderLayer;
public class Crosshair extends Module {
public static Property<Boolean> enabled = new Property<>(false);
public static Property<Boolean> range_indicator = new Property<>(false);
public static Property<Boolean> enabled = Property.bool(false);
public Crosshair() {
super("Crosshair", "crosshair", enabled.named("Enabled"), range_indicator.named("Range Indicator"));
super(
new ModuleDetails("Crosshair", "crosshair")
.description("Changes the crosshair if you're aiming at a entity")
.tags("Visuals", "Utility")
.version("v0.1.0"),
enabled.named("Enabled"));
}
@Override
public void render(RenderScope scope) {
if (enabled.value
&& range_indicator.value
&& SaturnClient.client.targetedEntity != null
&& SaturnClient.client.targetedEntity.isAlive()) {
int scaledWidth = 15;
int scaledHeight = 15;
scope.drawTexture(RenderLayer::getCrosshair, Textures.CROSSHAIR_RANGE,
scope.setRenderLayer(RenderLayer::getCrosshair);
scope.drawTexture(Textures.CROSSHAIR_RANGE,
(scope.getScaledWindowWidth() - scaledWidth) / 2,
(scope.getScaledWindowHeight() - scaledHeight) / 2, 0.0F, 0.0F, scaledWidth, scaledHeight,
scaledWidth, scaledHeight);
(scope.getScaledWindowHeight() - scaledHeight) / 2, 0.0F, 0.0F, scaledWidth, scaledHeight);
}
}
@@ -38,7 +42,7 @@ public class Crosshair extends Module {
}
@Override
public void setEnabled(boolean e) {
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,61 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class DayCounter extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 18);
long day = 0;
public DayCounter() {
super(new ModuleDetails("DayCounter", "day")
.description("Displays the number of days that have passed")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderDay(271, scope);
}
@Override
public void renderHud(RenderScope scope) {
day = (SaturnClient.client.world.getTimeOfDay() / 24000L);
renderDay(day, scope);
}
public void renderDay(long day, RenderScope scope) {
String text = "Day: " + day;
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,53 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class Fps extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
public Fps() {
super(new ModuleDetails("FPS Display", "fps")
.description("Displays current FPS")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
@Override
public void renderHud(RenderScope scope) {
String text = String.valueOf(SaturnClient.client.getCurrentFps()) + " FPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public void renderDummy(RenderScope scope) {
String text = "369 FPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,71 @@
package org.saturnclient.modules.mods;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import net.minecraft.client.option.Perspective;
public class Freelook extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(true);
public static Property<Integer> freelookKey = Property.keybinding(GLFW.GLFW_KEY_H);
public static boolean isFreeLooking = false;
private static boolean was1stPerson = false;
public Freelook() {
super(
new ModuleDetails("Freelook", "freelook")
.description("Allows you to look around freely without moving your character")
.tags("Movement", "Camera")
.version("0.1.0"),
enabled.named("Enabled"),
toggle.named("Toggle freelook"),
freelookKey.named("Freelook Keybinding")
);
}
@Override
public void tick() {
if (toggle.value) {
if (freelookKey.wasKeyPressed()) {
if (isFreeLooking) {
stopFreelook();
} else {
startFreelook();
}
}
} else if (freelookKey.isKeyPressed()) {
if (!isFreeLooking) {
startFreelook();
}
} else if (isFreeLooking) {
stopFreelook();
}
}
private void startFreelook() {
isFreeLooking = true;
was1stPerson = SaturnClient.client.options.getPerspective() == Perspective.FIRST_PERSON;
SaturnClient.client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
private void stopFreelook() {
if (was1stPerson) {
SaturnClient.client.options.setPerspective(Perspective.FIRST_PERSON);
}
isFreeLooking = false;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,39 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.manager.Property;
public class Fullbright extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> brightness = Property.integer(100);
public Fullbright() {
super(
new ModuleDetails("Fullbright", "fullbright")
.description("Allows you to see in the dark")
.tags("Camera")
.version("v0.1.0"),
enabled.named("Enabled"),
brightness.named("Brightness %"));
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
public static boolean shouldOverrideBrightness() {
return enabled.value;
}
public static float getBrightnessValue() {
return brightness.value / 10;
}
}
@@ -0,0 +1,85 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
public class HealthDisplay extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMode = Property.select(0, "Health", "Hearts");
private static Property<Integer> decimals = Property.select(0, "0", "1", "2");
private static ModDimensions dimensions = new ModDimensions(40, 18);
double speed = 0.;
Vec3d velocity;
double y = 0.;
public HealthDisplay() {
super(new ModuleDetails("Health display", "health")
.description("Displays your current health")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMode.named("Display mode"),
decimals.named("Decimals"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderHealth(10, scope);
}
@Override
public void renderHud(RenderScope scope) {
float health = SaturnClient.client.player.getHealth();
switch (displayMode.value) {
case 1:
health = health / 2;
}
renderHealth(health, scope);
}
public void renderHealth(float health, RenderScope scope) {
String text = "";
switch (decimals.value) {
case 0:
text = String.format("%.0f ", health);
break;
case 1:
text = String.format("%.1f ", health);
break;
case 2:
text = String.format("%.2f ", health);
break;
}
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,153 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.modules.ModuleDetails;
public class Keystrokes extends Module implements HudMod {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> showMouse = Property.bool(true);
private static final Property<Boolean> showSpace = Property.bool(false);
private static final Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static final Property<Integer> clickFg = Property.color(0xFFFFFFFF);
private static final ModDimensions dimensions = new ModDimensions(78, 54);
// Key states
private boolean w, a, s, d, lmb, rmb, space;
// Layout constants
private static final int KEY_SIZE = 24;
private static final int KEY_SPACING = 3;
private static final int MOUSE_WIDTH = 38;
private static final int MOUSE_HEIGHT = 24;
private static final int SPACE_HEIGHT = 19;
public Keystrokes() {
super(new ModuleDetails("Keystrokes", "keystrokes")
.description("Displays movement keystrokes")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
showMouse.named("Show mouse clicks"),
showSpace.named("Show space"),
dimensions.prop(),
clickFg.named("Clicked fg"),
clickBg.named("Clicked bg")
);
dimensions.renderBackground = false;
}
@Override
public void tick() {
w = SaturnClient.client.options.forwardKey.isPressed();
a = SaturnClient.client.options.leftKey.isPressed();
s = SaturnClient.client.options.backKey.isPressed();
d = SaturnClient.client.options.rightKey.isPressed();
lmb = SaturnClient.client.options.attackKey.isPressed();
rmb = SaturnClient.client.options.useKey.isPressed();
space = SaturnClient.client.options.jumpKey.isPressed();
updateHeight();
}
private void updateHeight() {
int height = KEY_SIZE * 2 + KEY_SPACING;
if (showSpace.value)
height += SPACE_HEIGHT + KEY_SPACING;
if (showMouse.value)
height += MOUSE_HEIGHT + KEY_SPACING;
dimensions.height = height;
}
@Override
public void renderDummy(RenderScope scope) {
render(scope, false);
}
@Override
public void renderHud(RenderScope scope) {
render(scope, true);
}
private void render(RenderScope scope, boolean live) {
int yOffset = 0;
// --- W ---
renderKey(scope, live && w, 'W', KEY_SIZE + KEY_SPACING, yOffset);
// --- A S D ---
yOffset += KEY_SIZE + KEY_SPACING;
renderKey(scope, live && a, 'A', 0, yOffset);
renderKey(scope, live && s, 'S', KEY_SIZE + KEY_SPACING, yOffset);
renderKey(scope, live && d, 'D', (KEY_SIZE + KEY_SPACING) * 2, yOffset);
yOffset += KEY_SIZE + KEY_SPACING;
// --- SPACE ---
if (showSpace.value) {
renderKeySpace(scope, live && space, 0, yOffset);
yOffset += SPACE_HEIGHT + KEY_SPACING;
}
// --- MOUSE ---
if (showMouse.value) {
renderKeyMouse(scope, live && lmb, "LMB", 0, yOffset);
renderKeyMouse(scope, live && rmb, "RMB", MOUSE_WIDTH + KEY_SPACING, yOffset);
}
}
private void renderKey(RenderScope scope, boolean pressed, char c, int x, int y) {
int bg = pressed ? clickBg.value : dimensions.bgColor.value;
int fg = pressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, KEY_SIZE, KEY_SIZE, dimensions.radius.value, bg);
scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, fg);
}
private void renderKeyMouse(RenderScope scope, boolean pressed, String text, int x, int y) {
int bg = pressed ? clickBg.value : dimensions.bgColor.value;
int fg = pressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, MOUSE_WIDTH, MOUSE_HEIGHT, dimensions.radius.value, bg);
scope.drawText(0.6f, text, x + 9, y + 7, dimensions.font.value, fg);
}
private void renderKeySpace(RenderScope scope, boolean pressed, int x, int y) {
int bg = pressed ? clickBg.value : dimensions.bgColor.value;
int fg = pressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, dimensions.width, SPACE_HEIGHT, dimensions.radius.value, bg);
int lineY = y + SPACE_HEIGHT / 2;
scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, fg);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,148 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.modules.mods.utils.HealthRenderState;
import org.saturnclient.saturnclient.config.manager.Property;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
// todo: rendering modes, custom names
public class Nametags extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> healthDisplay = Property.bool(false);
private static Property<Boolean> players = Property.bool(true);
private static Property<Boolean> hostile = Property.bool(false);
private static Property<Boolean> passive = Property.bool(false);
private static Property<Boolean> heartEmoji = Property.bool(true);
private static Property<Boolean> obfuscate = Property.bool(false);
private static Property<Integer> unit = Property.select(0, "Health", "Hearts");
private static Property<Integer> format = Property.select(0, "Value", "Value / Total", "%");
private static Property<Integer> nameColor = Property.select(15,
"Black",
"Dark Blue",
"Dark Green",
"Dark Aqua",
"Dark Red",
"Dark Purple",
"Gold",
"Gray",
"Dark Gray",
"Blue",
"Green",
"Aqua",
"Red",
"Light Purple",
"Yellow",
"White"
);
private static Property<Integer> healthColor = Property.select(12,
"Black",
"Dark Blue",
"Dark Green",
"Dark Aqua",
"Dark Red",
"Dark Purple",
"Gold",
"Gray",
"Dark Gray",
"Blue",
"Green",
"Aqua",
"Red",
"Light Purple",
"Yellow",
"White"
);
private static final String[] COLOR_CODES = {
"§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7",
"§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f"
};
public Nametags() {
super(
new ModuleDetails("Nametags", "nametags")
.description("Allows you to modify entity nametags")
.tags("Camera", "Utility")
.version("0.1.0"),
enabled.named("Enabled"),
healthDisplay.named("Display health"),
players.named("Players"),
hostile.named("Hostile"),
passive.named("Passive"),
heartEmoji.named("Heart emoji"),
obfuscate.named("Obfuscate enemy names"),
unit.named("Unit"),
format.named("Format"),
nameColor.named("Name color"),
healthColor.named("Health color")
);
}
@Override
public void tick() {
}
public static String getHealthString(LivingEntityRenderState state, HealthRenderState hrs, float health, float maxHealth) {
String ret = "";
switch (format.value){
case 0: // value
ret = String.format("%.1f", health / (unit.value + 1));
break;
case 1: // value / total
ret = String.format("%.1f", health / (unit.value + 1)) + " / " + String.format("%.1f", maxHealth / (unit.value + 1));
break;
case 2: // %
ret = String.format("%.0f%%", (health / maxHealth) * 100);
break;
}
return ret;
}
public static String getNametagString(LivingEntityRenderState state) {
if (state.customName == null) return null;
if (!healthDisplay.value) return state.customName.getString(); // name only
if (!(state instanceof HealthRenderState hrs)) return null;
float health = hrs.saturn$getHealth();
float maxHealth = hrs.saturn$getMaxHealth();
HealthRenderState.EntityType type = hrs.saturn$getEntityType();
if (type == HealthRenderState.EntityType.PASSIVE && !passive.value) return null;
if (type == HealthRenderState.EntityType.HOSTILE && !hostile.value) return null;
if (type == HealthRenderState.EntityType.PLAYER && !players.value) return null;
if (type == HealthRenderState.EntityType.OTHER) return null;
if (health < 0f || maxHealth < 0f || health > maxHealth) return null;
String hpColor = COLOR_CODES[healthColor.value];
String color = COLOR_CODES[nameColor.value];
String heart = heartEmoji.value ? " ❤" : "";
String obf = obfuscate.value ? "§k" : "";
String reset = "§r";
String ret = color + obf + state.customName.getString() + reset + " " + hpColor + getHealthString(state, hrs, health, maxHealth) + heart;
return ret;
}
public static boolean shouldReplaceName() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,39 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.manager.Property;
public class NoFog extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> liquids = Property.bool(false);
public NoFog() {
super(
new ModuleDetails("No Fog", "nofog")
.description("Stops fog from rendering")
.tags("Camera")
.version("v0.1.0"),
enabled.named("Enabled"),
liquids.named("See through liquids"));
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
// static for the mixin
public static boolean isActive() {
return enabled.value;
}
public static boolean liquids() {
return liquids.value;
}
}
@@ -0,0 +1,60 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayNetworkHandler;
public class Ping extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
public Ping() {
super(new ModuleDetails("Ping Display", "ping")
.description("Displays ping")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
@Override
public void renderHud(RenderScope scope) {
MinecraftClient client = MinecraftClient.getInstance();
ClientPlayNetworkHandler handler = client.getNetworkHandler();
String text = "0 ms";
if (handler != null) {
text = String.valueOf(handler.getPlayerListEntry(client.player.getUuid()).getLatency()) + " ms";
}
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public void renderDummy(RenderScope scope) {
String text = "10 ms";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,114 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
import net.minecraft.entity.Entity;
public class Speedometer extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> axis = Property.select(1, "Absolute", "X axis", "Y axis");
private static Property<Integer> unitText = Property.select(0, "None", "Blocks/s", "blocks/s", "b/s");
private static Property<Boolean> speedText = Property.bool(true);
private static ModDimensions dimensions = new ModDimensions(40, 18);
double speed = 0.;
Vec3d velocity;
double y = 0.;
public Speedometer() {
super(new ModuleDetails("Speedometer", "speed")
.description("Displays your current speed")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
axis.named("Speed type"),
unitText.named("Unit text"),
speedText.named("Show speed text"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderSpeed(2.71, scope);
}
@Override
public void renderHud(RenderScope scope) {
Entity vehicle = SaturnClient.client.player.getVehicle();
Entity player = SaturnClient.client.player;
if (vehicle == null) {
velocity = player.getVelocity();
y = player.isOnGround() ? 0. : velocity.y;
} else {
velocity = vehicle.getVelocity();
y = vehicle.isOnGround() ? 0. : velocity.y;
}
switch (axis.value) {
case 0: // Absolute
speed = (Math.sqrt(Math.pow(velocity.x, 2) + Math.pow(y, 2) + Math.pow(velocity.z, 2)));
break;
case 1: // x axis
speed = (Math.sqrt(Math.pow(velocity.x, 2) + Math.pow(velocity.z, 2)));
break;
case 2: // y axis
speed = (Math.abs(y));
break;
}
renderSpeed(speed * 20, scope); // blocks/tick to blocks/s
}
public void renderSpeed(double speed, RenderScope scope) {
String f = String.format("%.2f ", speed);
switch (unitText.value) {
case 1:
f += "Blocks/s";
break;
case 2:
f += "blocks/s";
break;
case 3:
f += "b/s";
break;
}
String text = f;
if (speedText.value) {
text = "Speed: " + text;
}
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,105 @@
package org.saturnclient.modules.mods;
import java.util.Arrays;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.texture.StatusEffectSpriteManager;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
public class StatusEffects extends Module implements HudMod {
public static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, 0);
private final StatusEffectInstance[] dummyEffects = {
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2),
};
public StatusEffects() {
super(new ModuleDetails("Status effects", "effect")
.description("Displays status effects")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
private void renderEffects(RenderScope scope, Iterable<StatusEffectInstance> effects) {
int row = 0;
StatusEffectSpriteManager statusEffectSpriteManager = SaturnClient.client.getStatusEffectSpriteManager();
for (StatusEffectInstance effect : effects) {
if (effect.shouldShowIcon()) {
RegistryEntry<StatusEffect> registryEntry = effect.getEffectType();
Sprite sprite = statusEffectSpriteManager.getSprite(registryEntry);
scope.drawSpriteStretched(sprite, 0, 18 * row, 16, 16);
scope.drawText(0.5f, getDurationAsString(effect), 18, 18 * row + 3, dimensions.font.value, -1);
row++;
}
}
dimensions.height = 18 * row;
}
@Override
public void renderHud(RenderScope scope) {
renderEffects(scope, SaturnClient.client.player.getStatusEffects());
}
@Override
public void renderDummy(RenderScope scope) {
renderEffects(scope, Arrays.asList(dummyEffects));
}
// This code was from: https://github.com/magicus/statuseffecttimer/blob/master/src/main/java/se/icus/mag/statuseffecttimer/mixin/StatusEffectTimerMixin.java
private String getDurationAsString(StatusEffectInstance statusEffectInstance) {
if (statusEffectInstance.isInfinite()) {
return I18n.translate("effect.duration.infinite");
}
int ticks = MathHelper.floor((float) statusEffectInstance.getDuration());
int totalSeconds = ticks / 20;
if (totalSeconds >= 3600) {
return totalSeconds / 3600 + "h";
} else if (totalSeconds >= 60) {
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return String.format("%d:%02d", minutes, seconds);
} else {
return String.valueOf(totalSeconds) + "s";
}
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,99 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class Tps extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
private static long lastWorldAge = -1;
private static long lastRealTime = -1;
private static double currentTps = 20.0;
private static final int SAMPLE_COUNT = 2;
private static final double[] samples = new double[SAMPLE_COUNT];
static {
java.util.Arrays.fill(samples, 20.0);
}
private static int sampleIndex = 0;
private static int samplesFilled = 0;
public Tps() {
super(new ModuleDetails("TPS Display", "tps")
.description("Displays the TPS")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop()
);
}
public static void onTimePacket(long worldAge) {
long now = System.currentTimeMillis();
if (lastWorldAge != -1 && lastRealTime != -1) {
long tickDelta = worldAge - lastWorldAge;
long timeDelta = now - lastRealTime;
if (timeDelta > 0 && tickDelta > 0) {
double sample = (tickDelta / (double) timeDelta) * 1000.0;
sample = Math.min(sample, 20.0);
samples[sampleIndex % SAMPLE_COUNT] = sample;
sampleIndex++;
samplesFilled = Math.min(samplesFilled + 1, SAMPLE_COUNT);
double sum = 0;
for (int i = 0; i < samplesFilled; i++) sum += samples[i];
currentTps = sum / samplesFilled;
}
}
lastWorldAge = worldAge;
lastRealTime = now;
}
public static void reset() {
lastWorldAge = -1;
lastRealTime = -1;
currentTps = 20.0;
sampleIndex = 0;
samplesFilled = SAMPLE_COUNT;
for (int i = 0; i < SAMPLE_COUNT; i++) samples[i] = 20.0;
}
@Override
public void renderHud(RenderScope scope) {
String text = String.format("%.1f TPS", currentTps);
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public void renderDummy(RenderScope scope) {
String text = "20.0 TPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,60 @@
package org.saturnclient.modules.mods;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.manager.Property;
public class Zoom extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(false);
public static Property<Integer> zoomKey = Property.keybinding(GLFW.GLFW_KEY_C);
public static Property<Float> zoomLevel = Property.floatProp(3.0f);
public static boolean isZooming = false;
public Zoom() {
super(
new ModuleDetails("Zoom", "zoom")
.description("Allows you to zoom in for a closer view")
.tags("Camera", "Utility")
.version("0.1.0"),
enabled.named("Enabled"),
toggle.named("Toggle zoom"),
zoomKey.named("Zoom Keybinding"),
zoomLevel.named("Zoom Level")
);
}
@Override
public void tick() {
if (toggle.value) {
if (zoomKey.wasKeyPressed()) {
isZooming = !isZooming;
}
} else {
isZooming = zoomKey.isKeyPressed();
}
}
public static float getZoomLevel() {
return zoomLevel.value;
}
public static boolean shouldZoom() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
if (!e) {
isZooming = false;
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,33 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.render.BackgroundRenderer;
import org.saturnclient.modules.mods.NoFog;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(BackgroundRenderer.class)
public class BackgroundRendererMixin {
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFog.isActive()) {
return Float.MAX_VALUE; // this could be customised
}
return viewDistance;
}
// liquids
@Redirect(
method = "applyFog",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isSpectator()Z")
)
private static boolean redirectIsSpectator(Entity entity) {
if (NoFog.isActive() && NoFog.liquids()) {
return true; // pretend to be spectator
}
return entity.isSpectator();
}
}
@@ -0,0 +1,44 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
import org.saturnclient.modules.mods.Freelook;
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing, float tickDelta, CallbackInfo ci) {
if (Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && MinecraftClient.getInstance().player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(MinecraftClient.getInstance().player.getPitch());
cameraOverriddenEntity.freelook$setCameraYaw(MinecraftClient.getInstance().player.getYaw());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(), cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -0,0 +1,62 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
import org.saturnclient.modules.mods.Freelook;
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
//noinspection ConstantValue// IntelliJ is incorrect here, this code block is reachable
if (Freelook.isFreeLooking && (Object) this instanceof ClientPlayerEntity) {
double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15);
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta;
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return this.cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return this.cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float pitch) {
this.cameraPitch = pitch;
}
@Override
@Unique
public void freelook$setCameraYaw(float yaw) {
this.cameraYaw = yaw;
}
}
@@ -0,0 +1,22 @@
package org.saturnclient.modules.mods.mixins;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.saturnclient.modules.mods.Zoom;
import net.minecraft.client.render.GameRenderer;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (Zoom.isZooming && Zoom.shouldZoom()) {
float originalFov = cir.getReturnValue();
float zoomedFov = originalFov / Zoom.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -0,0 +1,23 @@
// code modified from Gamma utils: https://github.com/Sjouwer/gamma-utils
package org.saturnclient.modules.mods.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager;
import org.saturnclient.modules.mods.Fullbright;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
@ModifyExpressionValue(
method = "update",
at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)
)
private float modifyGamma(float original) {
if (Fullbright.shouldOverrideBrightness()) {
return Fullbright.getBrightnessValue();
}
return original;
}
}
@@ -0,0 +1,26 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.modules.mods.utils.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements org.saturnclient.modules.mods.utils.HealthRenderState {
@Unique private float saturn$health = 0f;
@Unique private float saturn$maxHealth = 1f;
@Unique private HealthRenderState.EntityType saturn$entityType = HealthRenderState.EntityType.OTHER;
public float saturn$getHealth() { return saturn$health; }
public float saturn$getMaxHealth() { return saturn$maxHealth; }
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
public HealthRenderState.EntityType saturn$getEntityType() { return saturn$entityType; }
public void saturn$setEntityType(HealthRenderState.EntityType type) { this.saturn$entityType = type; }
}
@@ -0,0 +1,34 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.modules.mods.utils.HealthRenderState;
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.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(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());
if (entity instanceof PlayerEntity) {
hrs.saturn$setEntityType(HealthRenderState.EntityType.PLAYER);
} else if (entity instanceof HostileEntity) {
hrs.saturn$setEntityType(HealthRenderState.EntityType.HOSTILE);
} else if (entity instanceof PassiveEntity) {
hrs.saturn$setEntityType(HealthRenderState.EntityType.PASSIVE);
} else {
hrs.saturn$setEntityType(HealthRenderState.EntityType.OTHER);
}
}
}
@@ -0,0 +1,48 @@
package org.saturnclient.modules.mods.mixins;
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.modules.mods.Nametags;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
@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) {
if (saturn$rendering.get()) return;
if (!Nametags.shouldReplaceName()) return;
if (!(state instanceof LivingEntityRenderState living)) return;
String replacement = Nametags.getNametagString(living);
if (replacement == null) return;
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
}
@@ -0,0 +1,58 @@
package org.saturnclient.modules.mods.mixins;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderTickCounter;
@Mixin(InGameHud.class)
public class RenderMixin {
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
if (textRenderer != null &&
!(SaturnClient.client.currentScreen instanceof HudEditor)) {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (org.saturnclient.modules.Module m : ModManager.ENABLED_MODS) {
if (m instanceof HudMod && m.isEnabled()) {
ModDimensions dim = ((HudMod) m).getDimensions();
renderScope.matrices.push();
renderScope.matrices.translate(dim.x.value, (float) dim.y.value, 0);
renderScope.matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value,
dim.bgColor.value);
}
renderScope.setRenderLayer(RenderLayer::getGuiTextured);
((HudMod) m).renderHud(renderScope);
renderScope.matrices.pop();
} else {
m.render(renderScope);
}
}
}
}
}
@@ -0,0 +1,18 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
import org.saturnclient.modules.mods.Tps;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public class TpsMixin {
@Inject(method = "onWorldTimeUpdate", at = @At("HEAD"))
private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) {
Tps.onTimePacket(packet.time());
}
}
@@ -0,0 +1,9 @@
package org.saturnclient.modules.mods.utils;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -0,0 +1,14 @@
package org.saturnclient.modules.mods.utils;
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityType saturn$getEntityType();
void saturn$setEntityType(EntityType type);
enum EntityType {
PLAYER, HOSTILE, PASSIVE, OTHER
}
}
@@ -4,17 +4,22 @@ import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.TitleScreen;
import net.minecraft.client.util.math.MatrixStack;
import org.saturnclient.modules.ModManager;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.saturnclient.client.ServiceClient;
import org.saturnclient.saturnclient.config.Config;
import org.saturnclient.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.cosmetics.Emotes;
import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.saturnclient.menus.SaturnMenu;
import org.saturnclient.saturnclient.menus.SaturnTitleScreen;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.ui2.screens.TitleMenu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,25 +38,49 @@ public class SaturnClient implements ModInitializer {
public void onInitialize() {
LOGGER.info("Initializing " + MOD_ID);
client = MinecraftClient.getInstance();
Config.init();
ModManager.init();
SaturnClientConfig.init();
ThemeManager.load();
ConfigManager.load();
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> {ConfigManager.save(); ThemeManager.save();});
ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
if (SaturnClientConfig.saturnTitleScreen.value && screen instanceof TitleScreen
&& !(screen instanceof SaturnMenu)) {
client.setScreen(new SaturnTitleScreen());
}
client.execute(() -> {
SaturnScreen.preload(client);
});
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save());
SpecialModelLoaderEvents.LOAD_SCOPE.register(() -> {
return (resourceManager, location) -> MOD_ID.equals(location.getNamespace());
});
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
if (screen != null && !(screen instanceof TitleMenu)) {
if (screen instanceof SaturnScreen) {
((SaturnScreen) screen).draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18)
.position(screen.width - 180 - 20, screen.height - 18 - 20));
} else {
ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, tickDelta) -> {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
int width = context.getScaledWindowWidth();
int height = context.getScaledWindowHeight();
float scaleX = (float) screen1.width / width;
float scaleY = (float) screen1.height / height;
MatrixStack matrixStack = context.getMatrices();
matrixStack.push();
matrixStack.translate(width - 95, height - 14, 0);
matrixStack.scale(0.5f / scaleX, 0.5f / scaleY, 1.0f);
renderScope.drawTexture(Textures.LOGO_TEXT_BIG, 0, 0, 0, 0, 180, 18);
matrixStack.pop();
});
}
}
});
KeyInputHandler.register();
if (Auth.authenticate()) {
Emotes.initialize();
if (ServiceClient.authenticate()) {
Cloaks.initialize();
Hats.initialize();
LOGGER.info(MOD_ID + " initialization complete");
@@ -1,80 +0,0 @@
package org.saturnclient.saturnclient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.ui.SaturnAnimation;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.animations.FadeIn;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.ColorHelper;
public class SaturnClientConfig {
public static ConfigManager config;
public static Property<Integer> color =
new Property<>(
ColorHelper.getArgb(255, 251, 60, 79),
Property.PropertyType.HEX);
public static Property<Integer> normal =
new Property<>(
ColorHelper.getArgb(255, 182, 182, 182),
Property.PropertyType.HEX);
public static Property<Boolean> realisticLogo =
new Property<>(false);
public static Property<Boolean> saturnTitleScreen =
new Property<>(true);
public static int WHITE = ColorHelper.getArgb(255, 255, 255, 255);
public static int getWhite(float alpha) {
return ((int) (alpha * 255) << 24) | (normal.value & 0x00FFFFFF);
}
public static int getColor(boolean hovering, float alpha) {
return SaturnUi.getAlpha(hovering ? color.value : normal.value, alpha);
}
public static SaturnAnimation[] getAnimations() {
return new SaturnAnimation[] { new FadeIn(1) };
}
public static Identifier getLogo() {
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
}
public static String getSaturnIndicator() {
return realisticLogo.value ? "" : "";
}
/*
* Gets the icon color of a specific individual, here are the different colors
*
* - Owner: Dark Red
* - Admin: Red
* - Partners: Gold
* - Contributor: Aqua
* - Other/player: White
*/
public static Formatting getIconColor(String uuid) {
if (uuid.equals("d362a04228bd49e1a807ae74dbe8aba9")) {
return Formatting.DARK_RED; // Owner
}
return Formatting.WHITE;
}
public static void init() {
config = new ConfigManager("Saturn Client");
config.property("Primary color", color);
config.property("Default color", normal);
config.property("Realistic logo", realisticLogo);
config.property("Saturn client title screen", saturnTitleScreen);
}
}
@@ -1,192 +0,0 @@
package org.saturnclient.saturnclient.auth;
import java.util.HashMap;
import java.util.Map;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.saturnclient.SaturnClient;
public class Auth {
public static Map<String, SaturnPlayer> players = new HashMap<>();
public static Map<String, String> playerNames = new HashMap<>();
public static String uuid;
private static Thread listenerThread;
private static Thread pingThread;
private static volatile boolean running = false;
@SuppressWarnings("resource")
public static boolean authenticate() {
// Register shutdown hook ONCE
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close());
try {
if (SaturnClient.client.getSession() == null) {
SaturnClient.LOGGER.error("No active Minecraft session found");
return false;
}
String accessToken = SaturnClient.client.getSession().getAccessToken();
SaturnClient.LOGGER.info("Authenticating");
Network.init();
Network.write(accessToken);
String response = Network.read();
SaturnParser parser = new SaturnParser(response);
uuid = parser.getString("uuid");
String cloak = parser.getString("cloak");
String hat = parser.getString("hat");
for (String availableCloak : parser.getArray("cloaks")) {
Cloaks.availableCloaks.add(availableCloak);
}
for (String availableHat : parser.getArray("hats")) {
Hats.availableHats.add(availableHat);
}
players.put(uuid, new SaturnPlayer(cloak, hat));
Cloaks.loadCloak(uuid);
playerNames.put(SaturnClient.client.getSession().getUsername(), uuid);
afterAuth();
startListenerThread();
startPingThread();
return true;
} catch (Exception e) {
SaturnClient.LOGGER.error("Authentication failed", e);
return false;
}
}
private static void startListenerThread() {
running = true;
listenerThread = new Thread(() -> {
try {
while (running) {
String message = Network.read();
if (message == null) {
SaturnClient.LOGGER.error("Connection closed");
running = false;
break;
}
SaturnClient.LOGGER.info("Received: " + message);
SaturnParser parser = new SaturnParser(message);
switch (parser.method) {
case "player":
String uuid = parser.getString("uuid");
if (parser.getBool("saturn")) {
String cloak = parser.getString("cloak");
String hat = parser.getString("hat");
players.put(uuid, new SaturnPlayer(cloak, hat));
Cloaks.loadCloak(uuid);
} else {
playerNames.entrySet().removeIf(entry -> entry.getValue().equals(uuid));
}
break;
default:
if (parser.error != null) {
SaturnClient.LOGGER.error("Error from the server: " + parser.error);
}
break;
}
}
} catch (Exception e) {
SaturnClient.LOGGER.error("Error reading from server", e);
} finally {
close();
}
});
listenerThread.setDaemon(true);
listenerThread.start();
}
private static void startPingThread() {
pingThread = new Thread(() -> {
try {
while (running) {
Thread.sleep(25000); // 25 seconds
if (running) {
Network.write("ping");
}
}
} catch (InterruptedException e) {
SaturnClient.LOGGER.warn("Ping thread interrupted", e);
} catch (Exception e) {
SaturnClient.LOGGER.error("Error in ping thread", e);
}
});
pingThread.setDaemon(true);
pingThread.start();
}
public static void close() {
running = false;
if (pingThread != null) {
pingThread.interrupt();
}
if (listenerThread != null) {
listenerThread.interrupt();
}
try {
close();
SaturnClient.LOGGER.info("SaturnApi connection closed.");
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to close resources", e);
}
}
public static void afterAuth() {
PlayerTracker.initialize();
}
public static void setCloak(String cloakName) {
try {
Network.write("set_cloak@cloak=" + cloakName);
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void setHat(String hatName) {
try {
Network.write("set_hat@hat=" + hatName);
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void player(String name, String uuid) {
try {
playerNames.put(name, uuid.replaceAll("-", ""));
Network.write("player@uuid=" + uuid.replaceAll("-", ""));
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static void sendReload() {
for (Map.Entry<String, String> player : playerNames.entrySet()) {
if (players.containsKey(player.getValue()) && !player.getValue().equals(uuid)) {
SaturnClient.client.player.networkHandler
.sendChatCommand("msg " + player.getKey() + " "
+ "$SATURN_RELOAD if you are seeing this as a player, please report this to https://github.com/saturnclientmc/saturnclient/issues");
;
}
}
}
}
@@ -1,98 +0,0 @@
package org.saturnclient.saturnclient.auth;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.Base64;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.interfaces.*;
import java.security.spec.X509EncodedKeySpec;
public class Network {
static Socket socket;
static PrintWriter out;
static BufferedReader in;
static byte[] key = new byte[32];
public static class AES {
public static byte[] encryptAES256(String message, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encryptedMessage = cipher.doFinal(message.getBytes());
return encryptedMessage;
}
public static String decryptAES256(byte[] encryptedMessage, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedMessage = cipher.doFinal(encryptedMessage);
return new String(decryptedMessage);
}
}
public static void init() throws Exception {
// Generate AES key
new SecureRandom().nextBytes(key);
socket = new Socket("77.247.92.168", 8080);
// out = new DataOutputStream(socket.getOutputStream());
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
StringBuilder publicKeyBuilder = new StringBuilder();
String line;
while (!(line = in.readLine()).contains("-----END PUBLIC KEY-----")) {
publicKeyBuilder.append(line).append("\n");
}
publicKeyBuilder.append(line);
String publicKeyPem = publicKeyBuilder.toString();
String publicKeyBase64 = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "").replace("\n", "").replace("\r", "");
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedKey = rsaCipher.doFinal(key);
out.println(Base64.getEncoder().encodeToString(encryptedKey));
}
public static void write(String message) {
try {
out.println(Base64.getEncoder().encodeToString(AES.encryptAES256(message, key)));
} catch (Exception e) {
throw new RuntimeException("Failed to write to server: " + e.getMessage());
}
}
public static String read() {
try {
String msg = in.readLine();
if (msg != null) {
return AES.decryptAES256(Base64.getDecoder().decode(msg), key);
} else {
return "";
}
} catch (Exception e) {
return "";
}
}
public static void close() {
try {
socket.close();
out.close();
in.close();
} catch (Exception e) {
}
}
}
@@ -1,66 +0,0 @@
package org.saturnclient.saturnclient.auth;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class SaturnParser {
public String method = "";
public String error;
private Map<String, String> params = new HashMap<>();
public SaturnParser(String input) throws IOException {
String[] parts = input.split("@");
if (parts.length == 0) {
throw new IOException("Invalid input string");
}
if (parts[0].startsWith("!")) {
error = input;
}
method = parts[0];
for (int i = 1; i < parts.length; i++) {
String[] keyValue = parts[i].split("=");
if (keyValue.length == 2) {
params.put(keyValue[0].trim(), keyValue[1].trim());
} else if (keyValue.length == 1) {
params.put(keyValue[0].trim(), "");
} else {
throw new IOException("Invalid parameter format: " + parts[i]);
}
}
}
public String getString(String i) throws IOException {
String val = params.get(i);
if (val != null) {
return val;
} else {
throw new IOException("Parameter not found " + i);
}
}
public boolean getBool(String i) throws IOException {
String val = params.get(i);
if (val != null) {
return val.equals("true");
} else {
throw new IOException("Parameter not found " + i);
}
}
public String[] getArray(String i) throws IOException {
String val = params.get(i);
if (val != null) {
if (val.isEmpty()) {
return new String[] {};
}
return val.split("\\$");
} else {
throw new IOException("Parameter not found " + i);
}
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.saturnclient.auth;
public class SaturnPlayer {
public String cloak = "";
public String hat = "";
public SaturnPlayer(String cloak, String hat) {
this.cloak = cloak;
this.hat = hat;
}
}
@@ -0,0 +1,205 @@
package org.saturnclient.saturnclient.client;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import dev.selimaj.session.Session;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.util.Identifier;
public class ServiceClient {
private static Session session;
public static UUID uuid;
public static boolean connectTimeout() {
try {
session = Session.connect("wss://saturn-server.selimaj.dev", 10, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
return false;
}
}
public static boolean authenticate() {
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> {
if (session != null)
try {
session.close();
} catch (Exception e) {
SaturnClient.LOGGER.error("Unable to close Saturn session");
}
});
try {
var mcSession = SaturnClient.client.getSession();
if (mcSession == null) {
SaturnClient.LOGGER.error("No active Minecraft session found");
return false;
}
String accessToken = mcSession.getAccessToken();
uuid = mcSession.getUuidOrNull();
String username = mcSession.getUsername();
SaturnClient.LOGGER.info("Authenticating with UUID: " + uuid);
if (!connectTimeout()) {
SaturnClient.LOGGER.error("Unable to authenticate: Session Server Timeout");
return false;
}
ServiceMethods.Types.Player response = session.request(ServiceMethods.Authenticate, accessToken)
.get();
for (String availableCloak : response.cloaks()) {
Cloaks.availableCloaks.add(availableCloak);
}
for (String availableHat : response.hats()) {
Hats.availableHats.add(availableHat);
}
eventHandlers();
SaturnPlayer.set(new SaturnPlayer(uuid, username, response.cloak(), response.hat()));
Cloaks.loadCloak(uuid);
return true;
} catch (Exception e) {
SaturnClient.LOGGER.error("Authentication failed", e);
return false;
}
}
public static void setCloak(String itemId) {
try {
session.request(ServiceMethods.SetCloak, itemId).whenComplete((msg, throwable) -> {
if (throwable != null) {
throwable.printStackTrace();
} else {
try {
session.request(ServiceMethods.SendPlayer,
new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()));
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to set cloak (service): ", e);
}
}
public static void setHat(String itemId) {
try {
session.request(ServiceMethods.SetHat, itemId).whenComplete((msg, throwable) -> {
if (throwable != null) {
throwable.printStackTrace();
} else {
try {
session.request(ServiceMethods.SendPlayer,
new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()));
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to set hat (service): ", e);
}
}
public static void buyCloak(String itemId) {
try {
session.request(ServiceMethods.BuyCloak, itemId).whenComplete((msg, throwable) -> {
if (throwable != null) {
throwable.printStackTrace();
} else {
Cloaks.availableCloaks.add(itemId);
}
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to buy cloak (service): ", e);
}
}
public static void buyHat(String itemId) {
try {
session.request(ServiceMethods.BuyHat, itemId).whenComplete((msg, throwable) -> {
if (throwable != null) {
throwable.printStackTrace();
} else {
Hats.availableHats.add(itemId);
}
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to buy hat (service): ", e);
}
}
public static void emote(String emote) {
try {
session.request(ServiceMethods.Emote,
new ServiceMethods.Types.EmoteRequest(emote, SaturnPlayer.getExternalUUIDAsString()))
.whenComplete((msg, throwable) -> {
if (throwable != null) {
throwable.printStackTrace();
}
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to emote (service): ", e);
}
}
public static void eventHandlers() {
session.onNotification(ServiceMethods.EmoteEvent, (data) -> {
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
if (player.getUuidAsString().equals(data.from())) {
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
if (data.emote() != null && !data.emote().isEmpty()) {
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
animationStack.removeLayer(1000);
}
animationStack.addAnimLayer(1000,
PlayerAnimationRegistry
.getAnimation(Identifier.of("saturnclient", data.emote()))
.playAnimation());
} else {
animationStack.removeLayer(1000);
}
}
}
});
session.onNotification(ServiceMethods.Player, (player) -> {
System.out.println(player);
SaturnPlayer.set(player.toSaturnPlayer());
});
}
public static void player(UUID uuid, String name) {
try {
session.request(ServiceMethods.GetPlayer, uuid.toString())
.whenComplete((user, throwable) -> {
if (throwable != null) {
throwable.printStackTrace();
} else if (user != null) {
SaturnPlayer.set(user.toSaturnPlayer(uuid, name));
}
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to get player", e);
}
}
}
@@ -0,0 +1,113 @@
package org.saturnclient.saturnclient.client;
import java.util.UUID;
import org.saturnclient.saturnclient.client.ServiceMethods.Types.SendPlayerRequest;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import dev.selimaj.session.types.Method;
public class ServiceMethods {
// =========================
// Types (Rust structs)
// =========================
public static class Types {
public record EmoteRequest(String emote, String[] targets) {
}
public record EmoteEvent(String emote, String from) {
}
public record Player(
String cloak,
String hat,
String[] cloaks,
String[] hats) {
public SaturnPlayer toSaturnPlayer(UUID uuid, String name) {
return new SaturnPlayer(uuid, name, cloak, hat);
}
}
public record PlayerStream(Player player, String name, String uuid) {
public SaturnPlayer toSaturnPlayer() {
return new SaturnPlayer(UUID.fromString(uuid), name, this.player.cloak, this.player.hat);
}
}
public record SendPlayerRequest(String[] targets) {
}
}
// =========================
// AUTH
// =========================
public static final Method<String, Types.Player, String> Authenticate = new Method<>("auth",
String.class,
Types.Player.class,
String.class);
// =========================
// EQUIP
// =========================
public static final Method<String, String, String> SetCloak = new Method<>("set_cloak",
String.class,
String.class,
String.class);
public static final Method<String, String, String> SetHat = new Method<>("set_hat",
String.class,
String.class,
String.class);
// =========================
// BUY
// =========================
public static final Method<String, String, String> BuyCloak = new Method<>("buy_cloak",
String.class,
String.class,
String.class);
public static final Method<String, String, String> BuyHat = new Method<>("buy_hat",
String.class,
String.class,
String.class);
// =========================
// EMOTE
// =========================
public static final Method<Types.EmoteRequest, Void, String> Emote = new Method<>("emote",
Types.EmoteRequest.class,
Void.class,
String.class);
public static final Method<Types.EmoteEvent, Void, Void> EmoteEvent = new Method<>("emote_event",
Types.EmoteEvent.class,
Void.class,
Void.class);
// =========================
// GET PLAYER
// =========================
public static final Method<String, Types.Player, String> GetPlayer = new Method<>("get_player",
String.class,
Types.Player.class,
String.class);
// =========================
// PLAYER (Server Notification)
// =========================
public static final Method<Types.PlayerStream, Void, Void> Player = new Method<>("player",
Types.PlayerStream.class,
Void.class,
Void.class);
// =========================
// SEND PLAYER
// =========================
public static final Method<SendPlayerRequest, Void, String> SendPlayer = new Method<>("send_player",
SendPlayerRequest.class,
Void.class,
String.class);
}
@@ -1,8 +1,10 @@
package org.saturnclient.saturnclient.auth;
package org.saturnclient.saturnclient.client.player;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.client.ServiceClient;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.client.MinecraftClient;
@@ -45,8 +47,6 @@ public class PlayerTracker {
private static void onPlayerJoin(MinecraftClient client, PlayerListEntry player) {
GameProfile profile = player.getProfile();
String uuid = profile.getId().toString();
if (uuid != Auth.uuid)
Auth.player(profile.getName(), uuid);
ServiceClient.player(profile.getId(), profile.getName());
}
}
@@ -0,0 +1,61 @@
package org.saturnclient.saturnclient.client.player;
import java.util.Map;
import java.util.UUID;
import org.saturnclient.saturnclient.config.Config;
import net.minecraft.util.Formatting;
public class Roles {
static enum Role {
OWNER,
ADMIN,
PARTNER,
CONTRIBUTOR
}
private static final Map<UUID, Role> ROLES = Map.of(
UUID.fromString("d362a042-28bd-49e1-a807-ae74dbe8aba9"),
Role.OWNER,
UUID.fromString("d8dc7023-179b-4460-bcf6-493fbf1289a6"),
Role.CONTRIBUTOR);
/*
* Gets the icon color of a specific individual, here are the different colors
*
* - Owner: Dark Red
* - Admin: Red
* - Partners: Gold
* - Contributor: Aqua
* - Other/player: White
*/
public static Formatting getIconColor(UUID uuid) {
Role role = ROLES.get(uuid);
if (role == null) {
return Formatting.WHITE;
}
switch (role) {
case OWNER:
return Formatting.DARK_RED;
case ADMIN:
return Formatting.RED;
case PARTNER:
return Formatting.GOLD;
case CONTRIBUTOR:
return Formatting.AQUA;
default:
return Formatting.WHITE;
}
}
public static String getSaturnIndicator() {
return Config.realisticLogo.value ? "" : "";
}
}
@@ -0,0 +1,54 @@
package org.saturnclient.saturnclient.client.player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.saturnclient.client.ServiceClient;
public class SaturnPlayer {
private static Map<UUID, SaturnPlayer> PLAYERS = new HashMap<>();
private static Map<String, UUID> PLAYER_NAMES = new HashMap<>();
public String cloak = "";
public String hat = "";
public final UUID uuid;
public final String name;
public SaturnPlayer(UUID uuid, String name, String cloak, String hat) {
this.cloak = cloak;
this.hat = hat;
this.uuid = uuid;
this.name = name;
}
@Nullable
public static SaturnPlayer get() {
return PLAYERS.get(ServiceClient.uuid);
}
@Nullable
public static SaturnPlayer get(UUID uuid) {
return PLAYERS.get(uuid);
}
@Nullable
public static SaturnPlayer get(String name) {
UUID uuid = PLAYER_NAMES.get(name);
if (uuid == null)
return null;
return PLAYERS.get(uuid);
}
public static void set(SaturnPlayer player) {
PLAYER_NAMES.put(player.name, player.uuid);
PLAYERS.put(player.uuid, player);
}
public static String[] getExternalUUIDAsString() {
return PLAYERS.keySet().stream().filter(id -> !id.equals(ServiceClient.uuid)).map(UUID::toString)
.toArray(String[]::new);
}
}
@@ -0,0 +1,55 @@
package org.saturnclient.saturnclient.config;
import org.saturnclient.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.config.manager.Property;
public class AnimationConfig {
public static ConfigManager config;
// Logo
public static final AnimationConfig logo = new AnimationConfig(true, 700, 0);
// Main menu
public static final AnimationConfig mainMenu = new AnimationConfig(true, 700, 120);
// Mod menu
public static final AnimationConfig modMenu = new AnimationConfig(true, 100, 20);
// Shift menu
public static final AnimationConfig shiftMenu = new AnimationConfig(true, 100, 100);
// Cosmetics menu
public static final AnimationConfig cosmeticsMenu = new AnimationConfig(true, 700, 40);
// Emote wheel
public static final AnimationConfig emoteWheel = new AnimationConfig(true, 200, 40);
public static void init(ConfigManager parent) {
config = new ConfigManager(parent, "Animations");
logo.init("Logo");
mainMenu.init("Main Menu");
modMenu.init("Mod Menu");
shiftMenu.init("Shift Menu");
cosmeticsMenu.init("Cosmetics Menu");
emoteWheel.init("Emote Wheel");
}
// Animation properties
public final Property<Integer> duration;
public final Property<Integer> stagger;
public final Property<Integer> curve;
public AnimationConfig(boolean enabled, int delay, int stagger) {
this.duration = Property.integer(delay);
this.stagger = Property.integer(stagger);
this.curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
}
public void init(String name) {
// config.property("Enable " + name + " Animations", this.enabled);
config.property(name + " Duration", this.duration);
config.property(name + " Stagger", this.stagger);
config.property(name + " Curve", this.curve);
}
}
@@ -0,0 +1,37 @@
package org.saturnclient.saturnclient.config;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.config.manager.Property;
import org.saturnclient.ui2.resources.Textures;
import net.minecraft.util.Identifier;
public class Config {
public static ConfigManager config;
public static Property<Boolean> realisticLogo = Property.bool(false);
public static Property<Boolean> saturnTitleScreen = Property.bool(true);
public static Property<Boolean> cloakPhysics = Property.bool(true);
public static Property<Integer> openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B);
public static Property<Boolean> stagger = Property.bool(true);
public static Identifier getLogo() {
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
}
public static void init() {
config = new ConfigManager("Saturn Client");
config.property("Realistic logo", realisticLogo);
config.property("Saturn client title screen", saturnTitleScreen);
config.property("Open Emote Wheel", openEmoteWheel);
config.property("Cloak Physics", cloakPhysics);
config.property("Stagger Animations", stagger);
// Initialize a sub namespace for theme
Theme.init(config);
// Initialize a sub namespace for animations
AnimationConfig.init(config);
}
}
@@ -1,205 +0,0 @@
package org.saturnclient.saturnclient.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient;
public class ConfigManager {
private static File configFile = new File(
SaturnClient.client.runDirectory,
"saturn.json");
private static Map<String, Map<String, Property<?>>> properties = new HashMap<>();
private Map<String, Property<?>> currentMap;
public ConfigManager(String namespace) {
currentMap = new LinkedHashMap<>();
properties.put(namespace, currentMap);
System.out.println("Created namespace: " + namespace);
}
public ConfigManager(ConfigManager config, String namespace) {
currentMap = new LinkedHashMap<>();
Property<Map<String, Property<?>>> namespaceProperty = new Property<>(currentMap,
Property.PropertyType.NAMESPACE);
config.property(namespace, namespaceProperty);
// Update the parent's state
config.currentMap.put(namespace, namespaceProperty);
System.out.println("Created sub-namespace: " + namespace);
}
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> value) {
System.out.println("Adding property: " + name);
currentMap.put(name, value);
return value;
}
public static void load() {
try {
SaturnClient.LOGGER.info("Starting to load config...");
if (!configFile.exists()) {
configFile.createNewFile();
Files.write(configFile.toPath(), "{}".getBytes());
}
JsonObject jsonObject = JsonParser.parseString(
new String(Files.readAllBytes(configFile.toPath()))).getAsJsonObject();
for (String namespace : properties.keySet()) {
SaturnClient.LOGGER.info("Loading namespace: " + namespace);
JsonElement configElement = jsonObject.get(namespace);
if (configElement == null)
continue;
JsonObject config = jsonObject.get(namespace).getAsJsonObject();
if (config == null) {
continue;
}
Map<String, Property<?>> propertyMap = properties.get(namespace);
loadProperties(config, propertyMap);
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the config file", e);
}
}
@SuppressWarnings("unchecked")
private static void loadProperties(JsonObject config, Map<String, Property<?>> propertyMap) {
SaturnClient.LOGGER.info("Loading properties: " + propertyMap);
for (String propertyName : propertyMap.keySet()) {
JsonElement c = config.get(propertyName);
if (c == null) {
continue;
}
Property<?> p = propertyMap.get(propertyName);
if (p.matchesJson(c)) {
if (p.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedConfig = c.getAsJsonObject();
Map<String, Property<?>> nestedProperties = p.getNamespaceValue();
loadProperties(nestedConfig, nestedProperties);
} else {
// Handle regular properties
switch (p.getType()) {
case BOOLEAN:
((Property<Boolean>) p).value = c.getAsBoolean();
break;
case INTEGER:
((Property<Integer>) p).value = c.getAsInt();
break;
case FLOAT:
((Property<Float>) p).value = c.getAsFloat();
break;
case STRING:
((Property<String>) p).value = c.getAsString();
break;
case HEX:
((Property<Integer>) p).value = c.getAsInt();
break;
default:
break;
}
}
} else {
SaturnClient.LOGGER.warn(
"Property does not match JSON: " + propertyName);
}
}
}
public static void save() {
try {
SaturnClient.LOGGER.info("Starting to save config...");
JsonObject jsonObject = new JsonObject();
// Iterate through all namespaces and their properties
for (String namespace : properties.keySet()) {
JsonObject namespaceConfig = new JsonObject();
Map<String, Property<?>> propertyMap = properties.get(namespace);
// Save properties for this namespace
saveProperties(namespaceConfig, propertyMap);
// Add the namespace config to the main JSON object
jsonObject.add(namespace, namespaceConfig);
}
// Format the json so it can easily be edited
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String formattedJson = gson.toJson(jsonObject);
// Write the JSON object to the config file
Files.write(configFile.toPath(), formattedJson.getBytes());
SaturnClient.LOGGER.info("Config saved successfully.");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error saving the config file", e);
}
}
@SuppressWarnings("unchecked")
private static void saveProperties(JsonObject config, Map<String, Property<?>> propertyMap) {
// Iterate through each property in the namespace
for (String propertyName : propertyMap.keySet()) {
Property<?> property = propertyMap.get(propertyName);
JsonElement propertyValue = null;
if (property.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedConfig = new JsonObject();
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) property.value;
saveProperties(nestedConfig, nestedProperties);
propertyValue = nestedConfig;
} else {
// Convert the property value based on its type
switch (property.getType()) {
case BOOLEAN:
propertyValue = new JsonPrimitive((Boolean) property.value);
break;
case INTEGER:
propertyValue = new JsonPrimitive((Integer) property.value);
break;
case FLOAT:
propertyValue = new JsonPrimitive((Float) property.value);
break;
case STRING:
propertyValue = new JsonPrimitive((String) property.value);
break;
case HEX:
propertyValue = new JsonPrimitive((Integer) property.value);
break;
default:
SaturnClient.LOGGER.warn(
"Unknown property type for: " + propertyName);
break;
}
}
if (propertyValue != null) {
config.add(propertyName, propertyValue);
}
}
}
public Map<String, Property<?>> getProperties() {
return currentMap;
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.saturnclient.config;
public class NamedProperty<T> {
public String name;
public Property<T> prop;
public NamedProperty(String name, Property<T> value) {
this.name = name;
this.prop = value;
}
}
@@ -1,157 +0,0 @@
package org.saturnclient.saturnclient.config;
import java.util.Map;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
public class Property<T> {
public enum PropertyType {
BOOLEAN,
INTEGER,
FLOAT,
STRING,
HEX,
NAMESPACE
}
public T value;
public boolean isReset = false;
private T defaultValue;
private PropertyType type;
public Property(T value) {
this.value = value;
this.defaultValue = value;
if (value instanceof Boolean) {
type = PropertyType.BOOLEAN;
} else if (value instanceof Integer) {
type = PropertyType.INTEGER;
} else if (value instanceof Float) {
type = PropertyType.FLOAT;
} else if (value instanceof String) {
type = PropertyType.STRING;
} else if (valueIsNamespace(value)) {
type = PropertyType.NAMESPACE;
}
}
public Property(T value, PropertyType type) {
this.value = value;
this.defaultValue = value;
this.type = type;
}
public boolean getBool() {
if (value instanceof Boolean) {
return (boolean) value;
}
throw new IllegalStateException("Property does not contain a boolean");
}
public int getInt() {
if (value instanceof Integer) {
return (int) value;
}
throw new IllegalStateException("Property does not contain an int");
}
public float getFloat() {
if (value instanceof Float) {
return (float) value;
}
throw new IllegalStateException("Property does not contain a float");
}
public String getString() {
if (value instanceof String) {
return (String) value;
}
throw new IllegalStateException("Property does not contain a string");
}
public void reset() {
value = defaultValue;
isReset = true;
}
public int getHexString() {
if (value instanceof String) {
String str = (String) value;
if (str.startsWith("0x") || str.startsWith("0X")) {
try {
return Integer.parseInt(str.substring(2), 16);
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Invalid hexadecimal integer format");
}
}
}
throw new IllegalStateException(
"Property does not contain a valid hex integer");
}
public PropertyType getType() {
return type;
}
@SuppressWarnings("unchecked")
public Map<String, Property<?>> getNamespaceValue() {
if (type == PropertyType.NAMESPACE && value instanceof Map) {
return (Map<String, Property<?>>) value;
}
throw new IllegalStateException("Property is not a namespace");
}
public boolean matchesJson(JsonElement element) {
if (element.isJsonPrimitive()) {
JsonPrimitive primitive = element.getAsJsonPrimitive();
if (value instanceof Boolean && primitive.isBoolean())
return true;
if (value instanceof Integer && primitive.isNumber())
return true;
if (value instanceof Float && primitive.isNumber())
return true;
if (value instanceof String && primitive.isString())
return true;
// Check if JSON string represents a valid hex integer
if (value instanceof String && primitive.isString()) {
String str = primitive.getAsString();
return str.matches("0x[0-9A-Fa-f]+");
}
} else if (element.isJsonObject()) {
return isNamespace(value);
}
return false;
}
@Override
public String toString() {
return String.valueOf(value);
}
public boolean isNamespace(Object obj) {
return type == PropertyType.NAMESPACE && value instanceof Map;
}
public static boolean valueIsNamespace(Object obj) {
return obj instanceof Map;
}
@SuppressWarnings("unchecked")
public void setValue(Object value) {
this.value = (T) value;
}
@SuppressWarnings("unchecked")
public void setValue(Object value, Object defaultValue) {
this.value = (T) value;
this.defaultValue = (T) defaultValue;
}
public NamedProperty<T> named(String name) {
return new NamedProperty<>(name, this);
}
}
@@ -0,0 +1,91 @@
package org.saturnclient.saturnclient.config;
import org.saturnclient.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.config.manager.Property;
public class Theme {
// Main UI background (slightly warm dark to match Saturn tone)
public static final Property<Integer> BACKGROUND = Property.color(0xFF12100D);
// Default text color
public static final Property<Integer> FOREGROUND = Property.color(0xFFF2E6D2);
// Widget default background (normal state)
public static final Property<Integer> PRIMARY = Property.color(0xFF1C1915);
// Widget text color
public static final Property<Integer> PRIMARY_FG = Property.color(0xFFEAD9B8);
// Saturn brand color (hovered / enabled / selected)
public static final Property<Integer> ACCENT = Property.color(0xFFE2BF7D);
// Text/icons shown on accent background
public static final Property<Integer> ACCENT_FG = Property.color(0xFF1A140A);
// Misc colors
public static final Property<Integer> SCROLL = Property.color(0xFFE2BF7D);
// UI metrics
public static final Property<Integer> BG_RADIUS = Property.integer(12);
public static final Property<Integer> WIDGET_RADIUS = Property.integer(10);
// Font
public static final Property<Integer> FONT = Property.font(2);
public static void init(ConfigManager parent) {
ConfigManager config = new ConfigManager(parent, "UI Style");
config.property("Background", BACKGROUND);
config.property("Foreground", FOREGROUND);
config.property("Primary", PRIMARY);
config.property("Primary Foreground", PRIMARY_FG);
config.property("Accent", ACCENT);
config.property("Accent Foreground", ACCENT_FG);
config.property("Scrollbar Color", SCROLL);
config.property("Background Radius", BG_RADIUS);
config.property("Widget Radius", WIDGET_RADIUS);
config.property("Font", FONT);
}
/**
* primary by default, accent if true
*/
public static int getBg(boolean hover) {
return hover ? ACCENT.value : PRIMARY.value;
}
/**
* (Foreground) primary by default, accent if true
*/
public static int getFg(boolean hover) {
return hover ? ACCENT_FG.value : PRIMARY_FG.value;
}
/**
* Multiplies existing alpha with 0–255 alpha.
*/
public static int withAlpha(int alpha, int color) {
alpha = Math.max(0, Math.min(255, alpha));
int originalAlpha = (color >>> 24) & 0xFF;
// Merge (multiply) alphas
int merged = (originalAlpha * alpha) / 255;
return (color & 0x00FFFFFF) | (merged << 24);
}
/**
* Multiplies existing alpha with 0.0–1.0 alpha.
*/
public static int withAlpha(float alpha, int color) {
alpha = Math.max(0f, Math.min(1f, alpha));
int originalAlpha = (color >>> 24) & 0xFF;
int merged = (int) (originalAlpha * alpha);
return (color & 0x00FFFFFF) | (merged << 24);
}
}
@@ -1,224 +0,0 @@
package org.saturnclient.saturnclient.config;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
public class ThemeManager {
private static File themeFile = new File(
SaturnClient.client.runDirectory,
"saturn.theme.json");
private static Map<String, Map<String, Property<?>>> properties = new HashMap<>();
// private Map<String, Map<String, Property<?>>> states;
private Map<String, Property<?>> currentStyling;
private String state;
private String[] states;
private String namespace;
public ThemeManager(String namespace, String... states) {
// this.states = new LinkedHashMap<>();
this.namespace = namespace;
this.states = states;
currentStyling = new LinkedHashMap<>();
properties.put(namespace, new LinkedHashMap<>());
for (String s : states) {
properties.put(namespace+"@"+s, new LinkedHashMap<>());
}
}
public void setState(String state) {
this.state = state;
if (this.state != null) {
for (Map.Entry<String, Property<?>> tProp : properties.get(namespace+"@"+state).entrySet()) {
currentStyling.get(tProp.getKey()).setValue(tProp.getValue().value);
}
} else {
for (Map.Entry<String, Property<?>> tProp : properties.get(namespace).entrySet()) {
currentStyling.get(tProp.getKey()).setValue(tProp.getValue().value);
}
}
}
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> prop) {
for (String state : states) {
properties.get(namespace+"@"+state).put(name, new Property<T>(prop.value));
}
properties.get(namespace).put(name, new Property<T>(prop.value));
currentStyling.put(name, prop);
return prop;
}
public <T> void propertyStateDefault(String state, String name, T value) {
properties.get(namespace+"@"+state).get(name).setValue(value, value); // sets the current and default value
}
public static void load() {
try {
SaturnClient.LOGGER.info("Starting to load theme...");
if (!themeFile.exists()) {
themeFile.createNewFile();
Files.write(themeFile.toPath(), "{}".getBytes());
}
JsonObject jsonObject = JsonParser.parseString(
new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject();
for (String namespace : properties.keySet()) {
JsonElement themeElement = jsonObject.get(namespace);
if (themeElement == null)
continue;
JsonObject theme = jsonObject.get(namespace).getAsJsonObject();
if (theme == null) {
continue;
}
Map<String, Property<?>> propertyMap = properties.get(namespace);
loadProperties(theme, propertyMap);
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the theme file", e);
}
}
@SuppressWarnings("unchecked")
private static void loadProperties(JsonObject theme, Map<String, Property<?>> propertyMap) {
for (String propertyName : propertyMap.keySet()) {
JsonElement c = theme.get(propertyName);
if (c == null) {
continue;
}
Property<?> p = propertyMap.get(propertyName);
if (p.matchesJson(c)) {
if (p.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedTheme = c.getAsJsonObject();
Map<String, Property<?>> nestedProperties = p.getNamespaceValue();
loadProperties(nestedTheme, nestedProperties);
} else {
// Handle regular properties
switch (p.getType()) {
case BOOLEAN:
((Property<Boolean>) p).value = c.getAsBoolean();
break;
case INTEGER:
((Property<Integer>) p).value = c.getAsInt();
break;
case FLOAT:
((Property<Float>) p).value = c.getAsFloat();
break;
case STRING:
((Property<String>) p).value = c.getAsString();
break;
case HEX:
((Property<Integer>) p).value = c.getAsInt();
break;
default:
break;
}
}
} else {
SaturnClient.LOGGER.warn(
"Property does not match JSON: " + propertyName);
}
}
}
public static void save() {
try {
SaturnClient.LOGGER.info("Starting to save theme...");
JsonObject jsonObject = new JsonObject();
// Iterate through all namespaces and their properties
for (String namespace : properties.keySet()) {
JsonObject namespaceTheme = new JsonObject();
Map<String, Property<?>> propertyMap = properties.get(namespace);
// Save properties for this namespace
saveProperties(namespaceTheme, propertyMap);
// Add the namespace theme to the main JSON object
jsonObject.add(namespace, namespaceTheme);
}
// Format the json so it can easily be edited
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String formattedJson = gson.toJson(jsonObject);
// Write the JSON object to the theme file
Files.write(themeFile.toPath(), formattedJson.getBytes());
SaturnClient.LOGGER.info("Theme saved successfully.");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error saving the theme file", e);
}
}
@SuppressWarnings("unchecked")
private static void saveProperties(JsonObject theme, Map<String, Property<?>> propertyMap) {
// Iterate through each property in the namespace
for (String propertyName : propertyMap.keySet()) {
Property<?> property = propertyMap.get(propertyName);
JsonElement propertyValue = null;
if (property.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedTheme = new JsonObject();
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) property.value;
saveProperties(nestedTheme, nestedProperties);
propertyValue = nestedTheme;
} else {
// Convert the property value based on its type
switch (property.getType()) {
case BOOLEAN:
propertyValue = new JsonPrimitive((Boolean) property.value);
break;
case INTEGER:
propertyValue = new JsonPrimitive((Integer) property.value);
break;
case FLOAT:
propertyValue = new JsonPrimitive((Float) property.value);
break;
case STRING:
propertyValue = new JsonPrimitive((String) property.value);
break;
case HEX:
propertyValue = new JsonPrimitive((Integer) property.value);
break;
default:
SaturnClient.LOGGER.warn(
"Unknown property type for: " + propertyName);
break;
}
}
if (propertyValue != null) {
theme.add(propertyName, propertyValue);
}
}
}
}
@@ -0,0 +1,181 @@
package org.saturnclient.saturnclient.config.manager;
import com.google.gson.*;
import org.saturnclient.saturnclient.SaturnClient;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.Map;
public class ConfigManager {
private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json");
// SINGLE ROOT TREE
private static final Map<String, Property<?>> ROOT = new LinkedHashMap<>();
private static JsonObject cachedJson = null;
private final Map<String, Property<?>> currentMap;
private final String[] path;
/**
* Creates a root namespace
*/
public ConfigManager(String namespace) {
this.currentMap = new LinkedHashMap<>();
this.path = new String[] { namespace }; // Path is just ["namespace"]
ROOT.put(namespace, Property.namespace(currentMap));
}
/**
* Creates a sub-namespace inside a parent
*/
public ConfigManager(ConfigManager parent, String namespace) {
this.currentMap = new LinkedHashMap<>();
// Build new path: parent path + current namespace
this.path = new String[parent.path.length + 1];
System.arraycopy(parent.path, 0, this.path, 0, parent.path.length);
this.path[this.path.length - 1] = namespace;
parent.property(namespace, Property.namespace(currentMap));
}
/**
* Add a property
*/
public <T> Property<T> property(String name, Property<T> value) {
currentMap.put(name, value);
loadSingleProperty(name, value);
return value;
}
/**
* Add empty namespace
*/
public ConfigManager sub(String name) {
return new ConfigManager(this, name);
}
/**
* Load entire config
*/
public static void load() {
SaturnClient.LOGGER.info("Loading config...");
JsonObject json = loadAndCache();
if (json == null)
return;
loadRecursive(json, ROOT);
}
/**
* Save entire config
*/
public static void save() {
SaturnClient.LOGGER.info("Saving config...");
JsonObject rootJson = new JsonObject();
saveRecursive(rootJson, ROOT);
try {
String formatted = new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(rootJson);
Files.writeString(configFile.toPath(), formatted);
SaturnClient.LOGGER.info("Config saved successfully.");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error saving config file", e);
}
}
/**
* Recursive save
*/
private static void saveRecursive(JsonObject json, Map<String, Property<?>> map) {
map.forEach((name, prop) -> {
if (prop.isNamespace()) {
JsonObject child = new JsonObject();
saveRecursive(child, prop.getNamespaceMap());
json.add(name, child);
} else {
json.add(name, prop.toJson());
}
});
}
/**
* Recursive load
*/
private static void loadRecursive(JsonObject json, Map<String, Property<?>> map) {
map.forEach((name, prop) -> {
JsonElement element = json.get(name);
if (element == null)
return;
if (prop.isNamespace() && element.isJsonObject()) {
loadRecursive(element.getAsJsonObject(), prop.getNamespaceMap());
} else {
prop.loadFromJson(element);
}
});
}
/**
* Load only a single property immediately after creation,
* traversing the JSON tree based on this manager's path.
*/
private void loadSingleProperty(String name, Property<?> prop) {
JsonObject source = cachedJson != null ? cachedJson : loadAndCache();
if (source == null)
return;
// Traverse the tree to the correct namespace
JsonObject currentScope = source;
for (String segment : path) {
JsonElement next = currentScope.get(segment);
if (next != null && next.isJsonObject()) {
currentScope = next.getAsJsonObject();
} else {
return; // Path doesn't exist in config yet
}
}
// Now look for the property inside the correctly nested object
JsonElement element = currentScope.get(name);
if (element != null) {
prop.loadFromJson(element);
}
}
/**
* Reads file and caches json
*/
private static JsonObject loadAndCache() {
try {
if (!configFile.exists()) {
Files.writeString(configFile.toPath(), "{}");
return new JsonObject();
}
String json = Files.readString(configFile.toPath());
cachedJson = JsonParser.parseString(json).getAsJsonObject();
return cachedJson;
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading config file", e);
return null;
}
}
public Map<String, Property<?>> getProperties() {
return currentMap;
}
}
@@ -0,0 +1,298 @@
package org.saturnclient.saturnclient.config.manager;
import java.util.Map;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
public class Property<T> {
public static class NamedProperty<T> {
public String name;
public Property<T> prop;
public NamedProperty(String name, Property<T> value) {
this.name = name;
this.prop = value;
}
}
public enum PropertyType {
BOOLEAN,
INTEGER,
FLOAT,
STRING,
HEX,
NAMESPACE,
SELECT,
KEYBINDING,
}
public T value;
public boolean isReset = false;
private T defaultValue;
private String[] availableValues;
private final PropertyType type;
private boolean wasPressedLastTick = false;
private Property(T value, PropertyType type) {
this.value = value;
this.defaultValue = value;
this.type = type;
}
// ---------- Factory Methods ----------
public static <T> Property<T> from(T value) {
if (value instanceof Boolean)
return new Property<>(value, PropertyType.BOOLEAN);
if (value instanceof Integer)
return new Property<>(value, PropertyType.INTEGER);
if (value instanceof Float)
return new Property<>(value, PropertyType.FLOAT);
if (value instanceof String)
return new Property<>(value, PropertyType.STRING);
if (valueIsNamespace(value))
return new Property<>(value, PropertyType.NAMESPACE);
return null;
}
public static Property<Integer> font(int value) {
return select(value, "Default", "Inter", "Inter bold");
}
public static Property<Integer> select(Integer value, String... availableValues) {
Property<Integer> property = new Property<>(value, PropertyType.SELECT);
property.availableValues = availableValues;
return property;
}
public static Property<Map<String, Property<?>>> namespace(Map<String, Property<?>> value) {
return new Property<>(value, PropertyType.NAMESPACE);
}
public static Property<Boolean> bool(boolean value) {
return new Property<>(value, PropertyType.BOOLEAN);
}
public static Property<Integer> integer(int value) {
return new Property<>(value, PropertyType.INTEGER);
}
public static Property<Float> floatProp(float value) {
return new Property<>(value, PropertyType.FLOAT);
}
public static Property<String> string(String value) {
return new Property<>(value, PropertyType.STRING);
}
public static Property<Integer> color(int value) {
return new Property<>(value, PropertyType.HEX);
}
public static Property<Integer> keybinding(int value) {
return new Property<>(value, PropertyType.KEYBINDING);
}
// ---------- Select Helpers ----------
public void next() {
if (type == PropertyType.SELECT) {
int i = (Integer) value;
setValue((i < availableValues.length - 1) ? i + 1 : 0);
}
}
public void prev() {
if (type == PropertyType.SELECT) {
int i = (Integer) value;
setValue((i > 0) ? i - 1 : availableValues.length - 1);
}
}
public void setSelection(int selection) {
if (type == PropertyType.SELECT && selection >= 0 && selection < availableValues.length) {
setValue(selection);
}
}
public String getSelection() {
return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null;
}
// ---------- Lifecycle ----------
public void reset() {
value = defaultValue;
isReset = true;
}
public PropertyType getType() {
return type;
}
@SuppressWarnings("unchecked")
public Map<String, Property<?>> getNamespaceValue() {
if (type == PropertyType.NAMESPACE && value instanceof Map) {
return (Map<String, Property<?>>) value;
}
throw new IllegalStateException("Property is not a namespace");
}
// ---------- JSON Serialization ----------
/*
* BOOLEAN,
* INTEGER,
* FLOAT,
* STRING,
* HEX,
* NAMESPACE,
* SELECT,
* KEYBINDING,
*/
public JsonElement toJson() {
switch (type) {
case BOOLEAN:
return new JsonPrimitive((Boolean) value);
case INTEGER:
return new JsonPrimitive((Integer) value);
case FLOAT:
return new JsonPrimitive((Float) value);
case NAMESPACE:
JsonObject nested = new JsonObject();
getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson()));
return nested;
case HEX:
return new JsonPrimitive(String.format("#%08X", (Integer) value));
case KEYBINDING:
return new JsonPrimitive((Integer) value);
case SELECT:
return new JsonPrimitive((Integer) value);
default:
return new JsonPrimitive(String.valueOf(value));
}
}
public void loadFromJson(JsonElement element) {
if (element == null)
return;
switch (type) {
case BOOLEAN:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean())
setValue(element.getAsBoolean());
break;
case INTEGER:
if (element.isJsonPrimitive()) {
JsonPrimitive p = element.getAsJsonPrimitive();
if (p.isNumber())
setValue(p.getAsInt());
else if (p.isString())
setValue(parseHexToInt(p.getAsString()));
}
break;
case FLOAT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsFloat());
break;
case STRING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(element.getAsString());
break;
case HEX:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(parseHexToInt(element.getAsString()));
break;
case NAMESPACE:
if (element.isJsonObject()) {
JsonObject obj = element.getAsJsonObject();
getNamespaceValue().forEach((k, v) -> v.loadFromJson(obj.get(k)));
}
break;
case SELECT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setSelection(element.getAsInt());
break;
case KEYBINDING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsInt());
break;
}
}
// ---------- Utility ----------
@Override
public String toString() {
return String.valueOf(value);
}
public static boolean valueIsNamespace(Object obj) {
return obj instanceof Map;
}
@SuppressWarnings("unchecked")
public void setValue(Object value) {
this.value = (T) value;
}
@SuppressWarnings("unchecked")
public void setValue(Object value, Object defaultValue) {
this.value = (T) value;
this.defaultValue = (T) defaultValue;
}
public NamedProperty<T> named(String name) {
return new NamedProperty<>(name, this);
}
// ---------- Keybindings ----------
public boolean isKeyPressed() {
return (Integer) value != -1 &&
GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS &&
SaturnClient.client.currentScreen == null;
}
public boolean wasKeyPressed() {
boolean pressed = isKeyPressed();
boolean result = pressed && !wasPressedLastTick;
wasPressedLastTick = pressed;
return result;
}
// ---------- HEX ----------
public static int parseHexToInt(String hex) {
hex = hex.replace("#", "");
if (hex.length() == 6)
hex = "FF" + hex;
if (hex.length() != 8)
throw new IllegalArgumentException("Hex must be 6 or 8 chars long, got \'" + hex + "\'");
return (int) Long.parseLong(hex, 16);
}
// Misc
public Property<T> copy() {
return new Property<>(this.value, this.type);
}
public void load(String name, JsonElement element) {
if (element != null && element.isJsonObject()) {
load(name, element.getAsJsonObject());
}
}
public void load(String name, JsonObject element) {
JsonElement value = element.get(name);
if (value != null)
this.loadFromJson(value);
}
public boolean isNamespace() {
return type == PropertyType.NAMESPACE;
}
@SuppressWarnings("unchecked")
public Map<String, Property<?>> getNamespaceMap() {
return isNamespace() ? (Map<String, Property<?>>) (Object) value : null;
}
}
@@ -1,6 +1,9 @@
package org.saturnclient.saturnclient.cosmetics;
import org.saturnclient.saturnclient.auth.Auth;
import java.util.Arrays;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.saturnclient.config.Config;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import net.minecraft.client.render.OverlayTexture;
@@ -12,13 +15,8 @@ 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.BipedEntityModel;
import net.minecraft.client.render.entity.model.EntityModelLayers;
import net.minecraft.client.render.entity.model.LoadedEntityModels;
import net.minecraft.client.render.entity.model.PlayerCapeModel;
import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.SkinTextures;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.EquippableComponent;
@@ -26,21 +24,67 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.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 BipedEntityModel<PlayerEntityRenderState> model;
private final EquipmentModelLoader equipmentModelLoader;
private static final int PARTS = 16;
private final float[] segmentValues = new float[PARTS];
private static final float TEX_W = 176f;
private static final float TEX_H = 138f;
private static final class PxRect {
final int x, y, w, h;
PxRect(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
float u1() {
return x / TEX_W;
}
float v1() {
return y / TEX_H;
}
float u2() {
return (x + w) / TEX_W;
}
float v2() {
return (y + h) / TEX_H;
}
}
private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128);
private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128);
private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128);
private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128);
private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context,
LoadedEntityModels modelLoader, EquipmentModelLoader equipmentModelLoader) {
EquipmentModelLoader equipmentModelLoader) {
super(context);
this.model = new PlayerCapeModel<PlayerEntityRenderState>(
modelLoader.getModelPart(EntityModelLayers.PLAYER_CAPE));
this.equipmentModelLoader = equipmentModelLoader;
Arrays.fill(segmentValues, 0.0f);
}
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
EquippableComponent equippableComponent = (EquippableComponent) stack.get(DataComponentTypes.EQUIPPABLE);
EquippableComponent equippableComponent = (EquippableComponent) stack
.get(DataComponentTypes.EQUIPPABLE);
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
EquipmentModel equipmentModel = this.equipmentModelLoader
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get());
@@ -50,35 +94,241 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
}
}
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i,
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
if (!playerEntityRenderState.invisible && playerEntityRenderState.capeVisible) {
SkinTextures skinTextures = playerEntityRenderState.skinTextures;
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
float u1, float v1, float u2, float v2, int light, int overlay,
float normalX, float normalY, float normalZ, boolean flipWinding) {
MatrixStack.Entry entry = matrixStack.peek();
if (skinTextures.capeTexture() == null) {
String name = playerEntityRenderState.name;
String uuid = Auth.playerNames.get(name);
if (uuid == null || !Auth.players.containsKey(uuid)) {
return;
}
Identifier customCape = Cloaks.getCurrentCloakTexture(uuid);
if (customCape != null
&& !this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
matrixStack.push();
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) {
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
}
VertexConsumer vertexConsumer = vertexConsumerProvider
.getBuffer(RenderLayer.getEntityAlpha(customCape));
((PlayerEntityModel) this.getContextModel()).copyTransforms(this.model);
this.model.setAngles(playerEntityRenderState);
this.model.render(matrixStack, vertexConsumer, 0xF000F0, OverlayTexture.DEFAULT_UV);
matrixStack.pop();
}
}
if (!flipWinding) {
// CCW
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
(float) bottomLeft.z)
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
(float) bottomRight.z)
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
(float) topRight.z)
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
(float) topLeft.z)
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
} else {
// CW
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
(float) topLeft.z)
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
(float) topRight.z)
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
(float) bottomRight.z)
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
(float) bottomLeft.z)
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
}
}
private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer, PlayerEntityRenderState playerState,
int light, int overlay) {
float capeWidth = 10.0f / 16.0f;
float capeHeight = 16.0f / 16.0f;
float capeDepth = 1.0f / 16.0f;
float capePartHeight = capeHeight / PARTS;
float x1 = -capeWidth / 2.0f;
float x2 = capeWidth / 2.0f;
final float frontU1 = FRONT_RECT.u1();
final float frontU2 = FRONT_RECT.u2();
final float frontV1 = FRONT_RECT.v1();
final float frontV2 = FRONT_RECT.v2();
final float frontPartV = (frontV2 - frontV1) / PARTS;
final float backU1 = BACK_RECT.u1();
final float backU2 = BACK_RECT.u2();
final float backV1 = BACK_RECT.v1();
final float backV2 = BACK_RECT.v2();
final float backPartV = (backV2 - backV1) / PARTS;
final float leftU1 = LEFT_EDGE_RECT.u1();
final float leftU2 = LEFT_EDGE_RECT.u2();
final float leftV1 = LEFT_EDGE_RECT.v1();
final float leftV2 = LEFT_EDGE_RECT.v2();
final float leftPartV = (leftV2 - leftV1) / PARTS;
final float rightU1 = RIGHT_EDGE_RECT.u1();
final float rightU2 = RIGHT_EDGE_RECT.u2();
final float rightV1 = RIGHT_EDGE_RECT.v1();
final float rightV2 = RIGHT_EDGE_RECT.v2();
final float rightPartV = (rightV2 - rightV1) / PARTS;
final float topU1 = TOP_RECT.u1();
final float topU2 = TOP_RECT.u2();
final float topV1 = TOP_RECT.v1();
final float topV2 = TOP_RECT.v2();
final float bottomU1 = BOTTOM_RECT.u1();
final float bottomU2 = BOTTOM_RECT.u2();
final float bottomV1 = BOTTOM_RECT.v1();
final float bottomV2 = BOTTOM_RECT.v2();
float curY = 0.0f;
float curZ = 0.0f;
for (int i = 0; i < PARTS; i++) {
float angle = (2.0f - segmentValues[i]) * ((float) Math.PI / 2f);
float dirY = -(float) Math.cos(angle);
float dirZ = (float) Math.sin(angle);
// 1. Thickness at the START of this segment (Must match PREVIOUS segment's end)
float prevAngle = (i > 0) ? (2.0f - segmentValues[i - 1]) * ((float) Math.PI / 2f) : angle;
float startAvgAngle = (angle + prevAngle) / 2.0f;
float thickYStart = (float) Math.sin(startAvgAngle) * capeDepth;
float thickZStart = (float) Math.cos(startAvgAngle) * capeDepth;
// 2. Thickness at the END of this segment (Must match NEXT segment's start)
float nextAngle = (i < PARTS - 1) ? (2.0f - segmentValues[i + 1]) * ((float) Math.PI / 2f) : angle;
float endAvgAngle = (angle + nextAngle) / 2.0f;
float thickYEnd = (float) Math.sin(endAvgAngle) * capeDepth;
float thickZEnd = (float) Math.cos(endAvgAngle) * capeDepth;
float nextY = curY + dirY * capePartHeight;
float nextZ = curZ + dirZ * capePartHeight;
// Inner Vertices (The spine of the cape)
Vec3d innerTopLeft = new Vec3d(x2, curY, curZ);
Vec3d innerTopRight = new Vec3d(x1, curY, curZ);
Vec3d innerBotLeft = new Vec3d(x2, nextY, nextZ);
Vec3d innerBotRight = new Vec3d(x1, nextY, nextZ);
// Outer Vertices (Offset by the specific joint thickness)
Vec3d outerTopLeft = new Vec3d(x2, curY + thickYStart, curZ + thickZStart);
Vec3d outerTopRight = new Vec3d(x1, curY + thickYStart, curZ + thickZStart);
Vec3d outerBotLeft = new Vec3d(x2, nextY + thickYEnd, nextZ + thickZEnd);
Vec3d outerBotRight = new Vec3d(x1, nextY + thickYEnd, nextZ + thickZEnd);
// FRONT (Inner) - Changed winding/order to fix flipping
this.renderCapeQuad(vertexConsumer, matrixStack,
innerBotLeft, innerBotRight, innerTopRight, innerTopLeft,
frontU1, frontV1 + (frontPartV * i), frontU2, frontV1 + (frontPartV * (i + 1)),
light, overlay, 0, 0, -1, false);
// BACK (Outer)
this.renderCapeQuad(vertexConsumer, matrixStack,
outerBotRight, outerBotLeft, outerTopLeft, outerTopRight,
backU1, backV1 + (backPartV * i), backU2, backV1 + (backPartV * (i + 1)),
light, overlay, 0, 0, 1, false);
// LEFT EDGE
this.renderCapeQuad(vertexConsumer, matrixStack,
innerBotLeft, outerBotLeft, outerTopLeft, innerTopLeft,
leftU1, leftV1 + (leftPartV * i), leftU2, leftV1 + (leftPartV * (i + 1)),
light, overlay, 1, 0, 0, false);
// RIGHT EDGE
this.renderCapeQuad(vertexConsumer, matrixStack,
outerBotRight, innerBotRight, innerTopRight, outerTopRight,
rightU1, rightV1 + (rightPartV * i), rightU2, rightV1 + (rightPartV * (i + 1)),
light, overlay, -1, 0, 0, false);
// TOP FACE (Only on first segment)
if (i == 0) {
this.renderCapeQuad(vertexConsumer, matrixStack,
outerTopLeft, outerTopRight, innerTopRight, innerTopLeft,
topU1, topV1, topU2, topV2, light, overlay, 0, 1, 0, false);
}
// BOTTOM FACE (Only on last segment)
if (i == PARTS - 1) {
this.renderCapeQuad(vertexConsumer, matrixStack,
innerBotLeft, innerBotRight, outerBotRight, outerBotLeft,
bottomU1, bottomV1, bottomU2, bottomV2, light, overlay, 0, -1, 0, false);
}
curY = nextY;
curZ = nextZ;
}
}
// Shift array left and insert new value at the start
private void updateSegmentValues(float value) {
// Shift all elements one position to the right (from end to start)
for (int i = segmentValues.length - 1; i > 0; i--) {
segmentValues[i] = segmentValues[i - 1];
}
// Insert the new value at index 0
segmentValues[0] = value;
}
private long lastUpdate = 0;
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible
|| playerEntityRenderState.skinTextures.capeTexture() != null) {
return;
}
SaturnPlayer player = SaturnPlayer.get(playerEntityRenderState.name);
if (player == null) {
return;
}
Identifier customCape = Cloaks.getCurrentCloakTexture(player.uuid);
if (customCape == null
|| this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
return;
}
int minBrightness = 7;
int blockLight = (light >> 4) & 0xF;
int skyLight = (light >> 20) & 0xF;
blockLight = Math.max(blockLight, minBrightness);
skyLight = Math.max(skyLight, minBrightness);
light = (skyLight << 20) | (blockLight << 4);
VertexConsumer vertexConsumer = vertexConsumerProvider
.getBuffer(RenderLayer.getEntityAlpha(customCape));
matrixStack.push();
matrixStack.translate(0.0f, 0.0f, 0.19f);
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) {
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
}
long now = System.currentTimeMillis();
if (now - lastUpdate >= 20) {
float value = Math.min(1.0f, playerEntityRenderState.field_53537 / 108.0f)
+ (Math.max(0, playerEntityRenderState.field_53536) / 16);
if (Config.cloakPhysics.value) {
updateSegmentValues(value);
} else {
float last = segmentValues[0];
float maxStep = 0.02f;
float next = value;
if (next > last)
next = Math.min(next, last + maxStep);
else if (next < last)
next = Math.max(next, last - maxStep);
Arrays.fill(segmentValues, next);
}
lastUpdate = now;
}
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
matrixStack.pop();
}
}
@@ -0,0 +1,17 @@
package org.saturnclient.saturnclient.cosmetics;
import java.util.ArrayList;
import java.util.List;
public class Emotes {
public static List<String> availableEmotes = new ArrayList<>();
public static void initialize() {
availableEmotes.add("tpose");
availableEmotes.add("back_flip");
availableEmotes.add("facepalm");
availableEmotes.add("cry");
availableEmotes.add("giga_chad");
availableEmotes.add("front_flip");
}
}
@@ -1,8 +1,6 @@
package org.saturnclient.saturnclient.cosmetics;
import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
@@ -29,38 +27,36 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
PlayerEntityRenderState state, float limbAngle, float limbDistance) {
ItemStack headItem = state.equippedHeadStack;
String uuid = Auth.playerNames.get(state.name);
if (headItem.isEmpty() && uuid != null) {
SaturnPlayer player = Auth.players.get(uuid);
if (player == null || player.hat == null || player.hat.isEmpty()) {
return;
}
SaturnPlayer player = SaturnPlayer.get(state.name);
matrices.push();
this.getContextModel().head.rotate(matrices);
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
ItemStack customHat = new ItemStack(Items.STICK);
customHat.set(DataComponentTypes.ITEM_MODEL,
Identifier.of("saturnclient:hat_" + player.hat));
customHat.set(DataComponentTypes.EQUIPPABLE,
EquippableComponent.builder(EquipmentSlot.HEAD).build());
ItemRenderer itemRenderer = SaturnClient.client.getItemRenderer();
itemRenderer.renderItem(
customHat,
ModelTransformationMode.THIRD_PERSON_RIGHT_HAND,
light,
OverlayTexture.DEFAULT_UV,
matrices,
vertexConsumers,
null,
0);
matrices.pop();
if (!headItem.isEmpty() || state.invisible || player == null || player.hat.isEmpty()) {
return;
}
matrices.push();
this.getContextModel().head.rotate(matrices);
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
ItemStack customHat = new ItemStack(Items.STICK);
customHat.set(DataComponentTypes.ITEM_MODEL,
Identifier.of("saturnclient:hat_" + player.hat));
customHat.set(DataComponentTypes.EQUIPPABLE,
EquippableComponent.builder(EquipmentSlot.HEAD).build());
ItemRenderer itemRenderer = SaturnClient.client.getItemRenderer();
itemRenderer.renderItem(
customHat,
ModelTransformationMode.THIRD_PERSON_RIGHT_HAND,
light,
OverlayTexture.DEFAULT_UV,
matrices,
vertexConsumers,
null,
0);
matrices.pop();
}
}
@@ -2,27 +2,29 @@ package org.saturnclient.saturnclient.cosmetics;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.auth.Auth;
import java.util.UUID;
import org.saturnclient.saturnclient.client.ServiceClient;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
public class Hats {
public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo", "halo_black" };
public static List<String> availableHats = new ArrayList<>();
public static void initialize() {
availableHats.add(0, "");
}
public static void setHat(String uuid, String hatName) {
setHatSilent(uuid, hatName);
Auth.setHat(hatName);
Auth.sendReload();
public static void setHat(String hatName) {
if (availableHats.contains(hatName)) {
setHat(ServiceClient.uuid, hatName);
ServiceClient.setHat(hatName);
}
}
public static void setHatSilent(String uuid, String hatName) {
SaturnPlayer player = Auth.players.get(uuid);
if (player == null) {
Auth.players.put(uuid, new SaturnPlayer("", hatName));
} else {
public static void setHat(UUID uuid, String hatName) {
SaturnPlayer player = SaturnPlayer.get(uuid);
if (player != null) {
player.hat = hatName;
}
}
@@ -1,17 +1,22 @@
package org.saturnclient.saturnclient.cosmetics.cloaks;
import net.minecraft.util.Identifier;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.client.ServiceClient;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.saturnclient.cosmetics.cloaks.utils.AnimatedCloakData;
import org.saturnclient.saturnclient.cosmetics.cloaks.utils.IdentifierUtils;
import org.saturnclient.saturnclient.auth.SaturnPlayer;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.saturnclient.saturnclient.SaturnClient;
/**
@@ -20,13 +25,27 @@ import org.saturnclient.saturnclient.SaturnClient;
* Originally created by IIpho3nix and modified for Saturn Client by leo.
*/
public class Cloaks {
private static final String[] ANIMATED_CLOAKS = { "glitch" };
public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas",
"ferrari", "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white" };
private static final String[] ANIMATED_CLOAKS = { "glitch", "black_hole_amethyst", "black_hole_flame",
"black_hole_white" };
private static final String CLOAKS_RESOURCE_PATH = "assets/saturnclient/textures/cloaks/";
public static final List<String> availableCloaks = new ArrayList<>();
public static Identifier cloakCacheIdentifier = null;
public static final Map<String, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>();
private static final Map<String, Long> lastFrameTime = new ConcurrentHashMap<>();
public static final Map<UUID, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>();
private static final Map<UUID, Long> lastFrameTime = new ConcurrentHashMap<>();
private static final ExecutorService CLOAK_LOADER_EXECUTOR = Executors.newFixedThreadPool(
Math.min(4, Runtime.getRuntime().availableProcessors()),
r -> {
Thread t = new Thread(r, "cloak-loader");
t.setDaemon(true);
t.setPriority(Thread.NORM_PRIORITY - 1);
return t;
});
private static final ConcurrentHashMap<String, List<AnimatedCloakData>> CLOAK_CACHE = new ConcurrentHashMap<>();
/**
* Initializes the cloak system.
@@ -37,15 +56,15 @@ public class Cloaks {
}
/**
* Handles loading and caching of a new cloak texture.
* Sets cloak of the current player
*
* @param cloakName Name of the cloak file to load
*/
public static void setCloak(String uuid, String cloakName) {
Auth.setCloak(cloakName);
setCloakSilent(uuid, cloakName);
Auth.sendReload();
SaturnClient.LOGGER.info("Cloak set to " + cloakName);
public static void setCloak(String cloakName) {
if (availableCloaks.contains(cloakName)) {
setCloak(ServiceClient.uuid, cloakName);
ServiceClient.setCloak(cloakName);
}
}
/**
@@ -54,11 +73,10 @@ public class Cloaks {
*
* @param cloakName Name of the cloak file to load
*/
public static void setCloakSilent(String uuid, String cloakName) {
SaturnPlayer player = Auth.players.get(uuid);
if (player == null) {
Auth.players.put(uuid, new SaturnPlayer(cloakName, null));
} else {
public static void setCloak(UUID uuid, String cloakName) {
SaturnPlayer player = SaturnPlayer.get(uuid);
if (player != null) {
player.cloak = cloakName;
}
@@ -71,52 +89,19 @@ public class Cloaks {
*
* @param cloakName Name of the cloak file to load
*/
public static void loadCloak(String uuid) {
SaturnPlayer player = Auth.players.get(uuid);
public static void loadCloak(UUID uuid) {
SaturnPlayer player = SaturnPlayer.get(uuid);
if (player != null && player.cloak != null) {
if (!player.cloak.isEmpty()) {
if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) {
// Load animated cloak data in background thread
new Thread(() -> {
try {
String fileName = player.cloak + ".gif";
String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath);
loadAnimatedCloakAsync(uuid, player.cloak)
.exceptionally(throwable -> {
SaturnClient.LOGGER.error("Failed to load animated cloak for player: " + player.cloak,
throwable);
return null;
});
if (inputStream != null) {
byte[] data = inputStream.readAllBytes();
GifDecoder.GifImage gif = GifDecoder.read(data);
List<BufferedImage> frames = new ArrayList<>();
List<Integer> delays = new ArrayList<>();
int frameCount = gif.getFrameCount();
for (int i = 0; i < frameCount; i++) {
frames.add(gif.getFrame(i));
delays.add(gif.getDelay(i) * 10);
}
// Register textures on main thread
SaturnClient.client.execute(() -> {
try {
List<AnimatedCloakData> animatedFrames = new ArrayList<>();
for (int i = 0; i < frameCount; i++) {
String frameId = fileName.replace(".gif", "") + "_frame_" + i;
Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId);
IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frames.get(i));
animatedFrames.add(new AnimatedCloakData(frameIdentifier, delays.get(i)));
}
animatedCloaks.put(uuid, animatedFrames);
lastFrameTime.put(uuid, System.currentTimeMillis());
SaturnClient.LOGGER.info("Loaded " + frames.size() + " frames for animated cloak: " + fileName);
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register animated cloak textures: " + fileName, e);
}
});
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + player.cloak + ".gif", e);
}
}).start();
} else {
SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png"));
}
@@ -152,16 +137,94 @@ public class Cloaks {
}
}
public static Identifier getCurrentCloakTexture(String uuid) {
if (!Auth.players.containsKey(uuid)) {
public static CompletableFuture<Void> loadAnimatedCloakAsync(UUID uuid, String cloakName) {
return CompletableFuture.runAsync(() -> {
String fileName = cloakName + ".gif";
// Check cache first
List<AnimatedCloakData> cached = CLOAK_CACHE.get(fileName);
if (cached != null) {
animatedCloaks.put(uuid, cached);
lastFrameTime.put(uuid, System.currentTimeMillis());
return;
}
String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
try (InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath)) {
if (inputStream == null) {
SaturnClient.LOGGER.warn("Cloak resource not found: " + resourcePath);
return;
}
// Read all bytes at once - more efficient than multiple reads
byte[] data = inputStream.readAllBytes();
GifDecoder.GifImage gif = GifDecoder.read(data);
int frameCount = gif.getFrameCount();
if (frameCount == 0) {
SaturnClient.LOGGER.warn("No frames found in animated cloak: " + fileName);
return;
}
// Pre-allocate collections with known size
List<AnimatedCloakData> animatedFrames = new ArrayList<>(frameCount);
String baseFrameId = fileName.replace(".gif", "");
// Process frames in batch
for (int i = 0; i < frameCount; i++) {
BufferedImage frame = gif.getFrame(i);
int delay = Math.max(gif.getDelay(i) * 10, 50); // Minimum 50ms delay
String frameId = baseFrameId + "_frame_" + i;
Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId);
try {
IdentifierUtils.registerBufferedImageTextureFast(frameIdentifier, frame);
// Register texture on main thread if required by the graphics system
if (SaturnClient.client.isOnThread()) {
// IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame);
} else {
// Queue for main thread execution
// SaturnClient.client.execute(
// () -> IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame));
}
animatedFrames.add(new AnimatedCloakData(frameIdentifier, delay));
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register frame " + i + " for cloak: " + fileName, e);
// Continue with other frames instead of failing completely
}
}
if (!animatedFrames.isEmpty()) {
// Cache the result for future use
CLOAK_CACHE.put(fileName, animatedFrames);
animatedCloaks.put(uuid, animatedFrames);
lastFrameTime.put(uuid, System.currentTimeMillis());
SaturnClient.LOGGER.info("Loaded {} frames for animated cloak: {} (cached)",
animatedFrames.size(), fileName);
} else {
SaturnClient.LOGGER.error("No valid frames could be loaded for cloak: " + fileName);
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + fileName, e);
} catch (Exception e) {
SaturnClient.LOGGER.error("Unexpected error loading animated cloak: " + fileName, e);
}
}, CLOAK_LOADER_EXECUTOR);
}
public static Identifier getCurrentCloakTexture(UUID uuid) {
SaturnPlayer player = SaturnPlayer.get(uuid);
if (player == null || player.cloak.isEmpty()) {
return null;
}
String cloakName = Auth.players.get(uuid).cloak;
if (cloakName.isEmpty()) {
return null;
}
String cloakName = player.cloak;
if (Arrays.asList(ANIMATED_CLOAKS).contains(cloakName)) {
List<AnimatedCloakData> frames = animatedCloaks.get(uuid);
@@ -188,7 +251,13 @@ public class Cloaks {
currentFrame = 0;
}
return frames.get(currentFrame).getTextureId();
Identifier identifier = frames.get(currentFrame).getTextureId();
if (identifier == null) {
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
}
return identifier;
} else {
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
}
@@ -6,9 +6,10 @@ import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static java.lang.System.arraycopy;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
* Copyright 2014 Dhyan Blum
@@ -28,7 +29,7 @@ import static java.lang.System.arraycopy;
/**
* <p>
* A decoder capable of processing a GIF data stream to render the graphics
* A thread-safe decoder capable of processing a GIF data stream to render the graphics
* contained in it. This implementation follows the official
* <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF
* specification</A>.
@@ -54,9 +55,51 @@ import static java.lang.System.arraycopy;
* </p>
*
* @author Dhyan Blum
* @version 1.09 November 2017
* @version 1.09 November 2017 (Thread-safe version)
*/
public final class GifDecoder {
// Thread-local storage for reusable objects to avoid synchronization overhead
private static final ThreadLocal<DecodingContext> DECODING_CONTEXT =
ThreadLocal.withInitial(DecodingContext::new);
/**
* Thread-local context containing reusable objects for decoding operations.
* This eliminates the need for synchronization while maintaining performance benefits
* of object reuse within each thread.
*/
static final class DecodingContext {
final BitReader bitReader = new BitReader();
final CodeTable codeTable = new CodeTable();
int[] pixelBuffer;
int[] prevPixelBuffer;
int[] tempBuffer;
// Get or create a pixel buffer of the specified size
int[] getPixelBuffer(int size) {
if (pixelBuffer == null || pixelBuffer.length < size) {
pixelBuffer = new int[Math.max(size, 1024)]; // Minimum size to reduce allocations
}
return pixelBuffer;
}
// Get or create a previous pixel buffer of the specified size
int[] getPrevPixelBuffer(int size) {
if (prevPixelBuffer == null || prevPixelBuffer.length < size) {
prevPixelBuffer = new int[Math.max(size, 1024)];
}
return prevPixelBuffer;
}
// Get or create a temporary buffer of the specified size
int[] getTempBuffer(int size) {
if (tempBuffer == null || tempBuffer.length < size) {
tempBuffer = new int[Math.max(size, 1024)];
}
return tempBuffer;
}
}
static final class BitReader {
private int nextBitToRead;
private int numberOfBitsToRead;
@@ -71,14 +114,16 @@ public final class GifDecoder {
private int read() {
// Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8
int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8
int byte0, byte1, byte2;
byte0 = bytes[byteIndex++] & 0xFF; // & 0xFF gives us the unsigned values
byte1 = bytes[byteIndex++] & 0xFF;
byte2 = bytes[byteIndex] & 0xFF;
// Glue the bytes together, don't do more shifting than necessary
int buffer = ((byte2 << 8 | byte1) << 8 | byte0) >>> bitsToShiftRight;
final int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8
final int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8
// Use int operations to avoid repeated array access
final int byte0 = bytes[byteIndex] & 0xFF;
final int byte1 = bytes[byteIndex + 1] & 0xFF;
final int byte2 = bytes[byteIndex + 2] & 0xFF;
// Optimized bit manipulation
final int buffer = ((byte2 << 16) | (byte1 << 8) | byte0) >>> bitsToShiftRight;
nextBitToRead += numberOfBitsToRead;
return buffer & bitMask; // Kill the unwanted higher bits
}
@@ -100,7 +145,7 @@ public final class GifDecoder {
private BitReader bitReader; // Notify when code sizes increases
public CodeTable() {
table = new int[4096][1];
table = new int[4096][];
}
private int add(final int[] indices) {
@@ -130,14 +175,18 @@ public final class GifDecoder {
initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1
initTableSize = fr.endOfInfoCode + 1;
nextCode = initTableSize;
for (int c = numColors - 1; c >= 0; c--) {
table[c][0] = activeColTbl[c]; // Translated color
} // A gap may follow with no colors assigned if numCols < CLEAR
// Initialize single color entries
for (int c = 0; c < numColors; c++) {
table[c] = new int[]{activeColTbl[c]}; // Single element array
}
table[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR
table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
// Locate transparent color in code table and set to 0
if (fr.transpColFlag && fr.transpColIndex < numColors) {
table[fr.transpColIndex][0] = 0;
table[fr.transpColIndex] = new int[]{0};
}
}
}
@@ -145,73 +194,87 @@ public final class GifDecoder {
final class GifFrame {
// Graphic control extension (optional)
// Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
private int disposalMethod; // 0-3 as above, 4-7 undefined
private boolean transpColFlag; // 1 Bit
private int delay; // Unsigned, LSByte first, n * 1/100 * s
private int transpColIndex; // 1 Byte
private volatile int disposalMethod; // 0-3 as above, 4-7 undefined
private volatile boolean transpColFlag; // 1 Bit
private volatile int delay; // Unsigned, LSByte first, n * 1/100 * s
private volatile int transpColIndex; // 1 Byte
// Image descriptor
private int x; // Position on the canvas from the left
private int y; // Position on the canvas from the top
private int w; // May be smaller than the base image
private int h; // May be smaller than the base image
private int wh; // width * height
private boolean hasLocColTbl; // Has local color table? 1 Bit
private boolean interlaceFlag; // Is an interlace image? 1 Bit
private volatile int x; // Position on the canvas from the left
private volatile int y; // Position on the canvas from the top
private volatile int w; // May be smaller than the base image
private volatile int h; // May be smaller than the base image
private volatile int wh; // width * height
private volatile boolean hasLocColTbl; // Has local color table? 1 Bit
private volatile boolean interlaceFlag; // Is an interlace image? 1 Bit
@SuppressWarnings("unused")
private boolean sortFlag; // True if local colors are sorted, 1 Bit
private int sizeOfLocColTbl; // Size of the local color table, 3 Bits
private int[] localColTbl; // Local color table (optional)
private volatile boolean sortFlag; // True if local colors are sorted, 1 Bit
private volatile int sizeOfLocColTbl; // Size of the local color table, 3 Bits
private volatile int[] localColTbl; // Local color table (optional)
// Image data
private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
private int clearCode;
private int endOfInfoCode;
private byte[] data; // Holds LZW encoded data
private BufferedImage img; // Full drawn image, not just the frame area
private volatile int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
private volatile int clearCode;
private volatile int endOfInfoCode;
private volatile byte[] data; // Holds LZW encoded data
private volatile BufferedImage img; // Full drawn image, not just the frame area
// Synchronization for frame image creation
private final Object imageLock = new Object();
}
public final class GifImage {
public String header; // Bytes 0-5, GIF87a or GIF89a
private int w; // Unsigned 16 Bit, the least significant byte first
private int h; // Unsigned 16 Bit, the least significant byte first
private int wh; // Image width * image height
public boolean hasGlobColTbl; // 1 Bit
public int colorResolution; // 3 Bits
public boolean sortFlag; // True if global colors are sorted, 1 Bit
public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
public int bgColIndex; // Background color index, 1 Byte
public int pxAspectRatio; // Pixel aspect ratio, 1 Byte
public int[] globalColTbl; // Global color table
private final List<GifFrame> frames = new ArrayList<GifFrame>(64);
public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
public int repetitions = 0; // 0: infinite loop, N: number of loops
private BufferedImage img = null; // Currently, drawn frame
private final BitReader bits = new BitReader();
private final CodeTable codes = new CodeTable();
private Graphics2D g;
public volatile String header; // Bytes 0-5, GIF87a or GIF89a
private volatile int w; // Unsigned 16 Bit, the least significant byte first
private volatile int h; // Unsigned 16 Bit, the least significant byte first
private volatile int wh; // Image width * image height
public volatile boolean hasGlobColTbl; // 1 Bit
public volatile int colorResolution; // 3 Bits
public volatile boolean sortFlag; // True if global colors are sorted, 1 Bit
public volatile int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
public volatile int bgColIndex; // Background color index, 1 Byte
public volatile int pxAspectRatio; // Pixel aspect ratio, 1 Byte
public volatile int[] globalColTbl; // Global color table
private final List<GifFrame> frames = new ArrayList<>(64);
public volatile String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
public volatile String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
public volatile int repetitions = 0; // 0: infinite loop, N: number of loops
// Thread-safe frame rendering state
private volatile BufferedImage img = null; // Currently, drawn frame
private volatile Graphics2D g;
private final ReadWriteLock renderLock = new ReentrantReadWriteLock();
// Synchronization objects
private final Object initLock = new Object();
private int[] decode(final GifFrame fr, final int[] activeColTbl) {
codes.init(fr, activeColTbl, bits);
bits.init(fr.data); // Incoming codes
final DecodingContext ctx = DECODING_CONTEXT.get();
ctx.codeTable.init(fr, activeColTbl, ctx.bitReader);
ctx.bitReader.init(fr.data); // Incoming codes
final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
final int[] out = new int[wh]; // Target image pixel array
final int[][] tbl = codes.table; // Code table
// Use thread-local pixel buffer
final int[] out = ctx.getPixelBuffer(wh);
final int[][] tbl = ctx.codeTable.table; // Code table
int outPos = 0; // Next pixel position in the output image array
codes.clear(); // Init code table
bits.read(); // Skip leading clear code
int code = bits.read(); // Read first code
ctx.codeTable.clear(); // Init code table
ctx.bitReader.read(); // Skip leading clear code
int code = ctx.bitReader.read(); // Read first code
int[] pixels = tbl[code]; // Output pixel for first code
arraycopy(pixels, 0, out, outPos, pixels.length);
// Use System.arraycopy for better performance
System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length;
try {
while (true) {
final int prevCode = code;
code = bits.read(); // Get next code in stream
code = ctx.bitReader.read(); // Get next code in stream
if (code == clearCode) { // After a CLEAR table, there is
codes.clear(); // no previous code, we need to read
code = bits.read(); // a new one
ctx.codeTable.clear(); // no previous code, we need to read
code = ctx.bitReader.read(); // a new one
pixels = tbl[code]; // Output pixels
arraycopy(pixels, 0, out, outPos, pixels.length);
System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length;
continue; // Back to the loop with a valid previous code
} else if (code == endCode) {
@@ -219,92 +282,107 @@ public final class GifDecoder {
}
final int[] prevVals = tbl[prevCode];
final int[] prevValsAndK = new int[prevVals.length + 1];
arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
if (code < codes.nextCode) { // Code table contains code
System.arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
if (code < ctx.codeTable.nextCode) { // Code table contains code
pixels = tbl[code]; // Output pixels
arraycopy(pixels, 0, out, outPos, pixels.length);
System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length;
prevValsAndK[prevVals.length] = tbl[code][0]; // K
} else {
prevValsAndK[prevVals.length] = prevVals[0]; // K
arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
System.arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
outPos += prevValsAndK.length;
}
codes.add(prevValsAndK); // Previous indices + K
ctx.codeTable.add(prevValsAndK); // Previous indices + K
}
} catch (final ArrayIndexOutOfBoundsException ignored) {
}
return out;
// Return a properly sized array
return Arrays.copyOf(out, outPos);
}
private int[] deinterlace(final int[] src, final GifFrame fr) {
final int w = fr.w, h = fr.h, wh = fr.wh;
final int[] dest = new int[src.length];
final DecodingContext ctx = DECODING_CONTEXT.get();
final int[] dest = ctx.getTempBuffer(src.length);
// Interlaced images are organized in 4 sets of pixel lines
final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0)
final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
// Sets' start indices in source array
final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
// Line skips in destination array
// Line skips in destination array - use bit shifts for multiplication
final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
// Group 1 contains every 8th line starting from 0
int from = 0, to = 0;
for (; from < set2; from += w, to += w8) {
arraycopy(src, from, dest, to, w);
} // Group 2 contains every 8th line starting from 4
System.arraycopy(src, from, dest, to, w);
}
// Group 2 contains every 8th line starting from 4
for (to = w4; from < set3; from += w, to += w8) {
arraycopy(src, from, dest, to, w);
} // Group 3 contains every 4th line starting from 2
System.arraycopy(src, from, dest, to, w);
}
// Group 3 contains every 4th line starting from 2
for (to = w2; from < set4; from += w, to += w4) {
arraycopy(src, from, dest, to, w);
} // Group 4 contains every 2nd line starting from 1 (biggest group)
System.arraycopy(src, from, dest, to, w);
}
// Group 4 contains every 2nd line starting from 1 (biggest group)
for (to = w; from < wh; from += w, to += w2) {
arraycopy(src, from, dest, to, w);
System.arraycopy(src, from, dest, to, w);
}
return dest; // All pixel lines have now been rearranged
return Arrays.copyOf(dest, src.length); // Return a copy, not the shared buffer
}
private void drawFrame(final GifFrame fr) {
// Determine the color table that will be active for this frame
final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
// Get pixels from data stream
int[] pixels = decode(fr, activeColTbl);
if (fr.interlaceFlag) {
pixels = deinterlace(pixels, fr); // Rearrange pixel lines
}
// Create image of type 2=ARGB for frame area
final BufferedImage frame = new BufferedImage(fr.w, fr.h, 2);
arraycopy(pixels, 0, ((DataBufferInt) frame.getRaster().getDataBuffer()).getData(), 0, fr.wh);
// Draw frame area on top of working image
g.drawImage(frame, fr.x, fr.y, null);
renderLock.writeLock().lock();
try {
// Determine the color table that will be active for this frame
final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
// Get pixels from data stream
int[] pixels = decode(fr, activeColTbl);
if (fr.interlaceFlag) {
pixels = deinterlace(pixels, fr); // Rearrange pixel lines
}
// Create image of type TYPE_INT_ARGB for frame area
final BufferedImage frame = new BufferedImage(fr.w, fr.h, BufferedImage.TYPE_INT_ARGB);
final int[] frameData = ((DataBufferInt) frame.getRaster().getDataBuffer()).getData();
System.arraycopy(pixels, 0, frameData, 0, Math.min(pixels.length, fr.wh));
// Draw frame area on top of working image
g.drawImage(frame, fr.x, fr.y, null);
// Visualize frame boundaries during testing
// if (DEBUG_MODE) {
// if (prev != null) {
// g.setColor(Color.RED); // Previous frame color
// g.drawRect(prev.x, prev.y, prev.w - 1, prev.h - 1);
// }
// g.setColor(Color.GREEN); // New frame color
// g.drawRect(fr.x, fr.y, fr.w - 1, fr.h - 1);
// }
// Keep a copy of the previous frame's pixels
final DecodingContext ctx = DECODING_CONTEXT.get();
final int[] prevPixelBuffer = ctx.getPrevPixelBuffer(wh);
final int[] imgData = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
System.arraycopy(imgData, 0, prevPixelBuffer, 0, wh);
// Keep a copy of the previous frame's pixels in case we need to restore the
// frame
int[] prevPx = new int[wh];
arraycopy(((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, prevPx, 0, wh);
// Create another copy for the end user to not expose internal state
synchronized (fr.imageLock) {
if (fr.img == null) { // Double-checked locking
fr.img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
final int[] frameImgData = ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData();
System.arraycopy(prevPixelBuffer, 0, frameImgData, 0, wh);
}
}
// Create another copy for the end user to not expose internal state
fr.img = new BufferedImage(w, h, 2); // 2 = ARGB
arraycopy(prevPx, 0, ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(), 0, wh);
// Handle disposal of current frame
if (fr.disposalMethod == 2) {
// Restore to background color (clear frame area only)
g.clearRect(fr.x, fr.y, fr.w, fr.h);
} else if (fr.disposalMethod == 3) {
// Restore previous frame
arraycopy(prevPx, 0, ((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, wh);
// Handle disposal of current frame
if (fr.disposalMethod == 2) {
// Restore to background color (clear frame area only)
g.clearRect(fr.x, fr.y, fr.w, fr.h);
} else if (fr.disposalMethod == 3) {
// Restore previous frame
System.arraycopy(prevPixelBuffer, 0, imgData, 0, wh);
}
} finally {
renderLock.writeLock().unlock();
}
}
@@ -317,13 +395,18 @@ public final class GifDecoder {
* @return 32 bit ARGB color in the form 0xAARRGGBB
*/
public final int getBackgroundColor() {
final GifFrame frame = frames.get(0);
if (frame.hasLocColTbl) {
return frame.localColTbl[bgColIndex];
} else if (hasGlobColTbl) {
return globalColTbl[bgColIndex];
renderLock.readLock().lock();
try {
final GifFrame frame = frames.get(0);
if (frame.hasLocColTbl) {
return frame.localColTbl[bgColIndex];
} else if (hasGlobColTbl) {
return globalColTbl[bgColIndex];
}
return 0;
} finally {
renderLock.readLock().unlock();
}
return 0;
}
/**
@@ -334,7 +417,12 @@ public final class GifDecoder {
* @return Delay as number of hundredths (1/100) of a second
*/
public final int getDelay(final int index) {
return frames.get(index).delay;
renderLock.readLock().lock();
try {
return frames.get(index).delay;
} finally {
renderLock.readLock().unlock();
}
}
/**
@@ -349,21 +437,37 @@ public final class GifDecoder {
* @return A BufferedImage for the specified frame.
*/
public BufferedImage getFrame(final int index) {
if (img == null) { // Init
img = new BufferedImage(w, h, 2); // 2 = ARGB
g = img.createGraphics();
g.setBackground(new Color(0, true)); // Transparent color
}
GifFrame fr = frames.get(index);
if (fr.img == null) {
// Draw all frames until and including the requested frame
for (int i = 0; i <= index; i++) {
fr = frames.get(i);
if (fr.img == null) {
drawFrame(fr);
// Initialize rendering context if needed
if (img == null) {
synchronized (initLock) {
if (img == null) { // Double-checked locking
img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g = img.createGraphics();
g.setBackground(new Color(0, true)); // Transparent color
// Set rendering hints for better performance
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
}
}
}
GifFrame fr = frames.get(index);
// Check if frame is already rendered using double-checked locking
if (fr.img == null) {
synchronized (fr.imageLock) {
if (fr.img == null) {
// Draw all frames until and including the requested frame
for (int i = 0; i <= index; i++) {
fr = frames.get(i);
if (fr.img == null) {
drawFrame(fr);
}
}
}
}
}
return fr.img;
}
@@ -407,6 +511,8 @@ public final class GifDecoder {
img.globalColTbl = new int[img.sizeOfGlobColTbl];
pos = readColTbl(in, img.globalColTbl, pos);
}
// Main parsing loop with bounds checking optimization
while (pos < in.length) {
final int block = in[pos] & 0xFF;
switch (block) {
@@ -456,7 +562,7 @@ public final class GifDecoder {
// and wait for a valid block. Experience: It'll get worse. b)
// Throw exception. c) Return gracefully if we are almost done
// processing. The frames we have so far should be error-free.
final double progress = 1.0 * pos / in.length;
final double progress = (double) pos / in.length;
if (progress < 0.9) {
throw new IOException("Unknown block at: " + pos);
}
@@ -489,6 +595,7 @@ public final class GifDecoder {
* @return Index of the first byte after this extension
*/
static int readAppExt(final GifImage img, final byte[] in, int i) {
// Use StandardCharsets for better performance if available, otherwise fallback
img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
i += 14; // Go to sub-block size, it's value should be 3
@@ -496,7 +603,7 @@ public final class GifDecoder {
// The only app extension widely used is NETSCAPE, it's got 3 data bytes
if (subBlockSize == 3) {
// in[i+1] should have value 01, in[i+5] should be block terminator
img.repetitions = in[i + 2] & 0xFF | in[i + 3] & 0xFF << 8; // Short
img.repetitions = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // Short
return i + 5;
} // Skip unknown application extensions
while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
@@ -514,11 +621,11 @@ public final class GifDecoder {
static int readColTbl(final byte[] in, final int[] colors, int i) {
final int numColors = colors.length;
for (int c = 0; c < numColors; c++) {
final int a = 0xFF; // Alpha 255 (opaque)
final int r = in[i++] & 0xFF; // 1st byte is red
final int g = in[i++] & 0xFF; // 2nd byte is green
final int b = in[i++] & 0xFF; // 3rd byte is blue
colors[c] = ((a << 8 | r) << 8 | g) << 8 | b;
// Optimized color packing - alpha is always 0xFF for opaque
colors[c] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
return i;
}
@@ -530,9 +637,10 @@ public final class GifDecoder {
* @return Index of the first byte after this block
*/
static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2
fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0
fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB
final int packed = in[i + 3] & 0xFF;
fr.disposalMethod = (packed & 0b00011100) >>> 2; // Bits 4-2
fr.transpColFlag = (packed & 1) == 1; // Bit 0
fr.delay = (in[i + 4] & 0xFF) | ((in[i + 5] & 0xFF) << 8); // 16 bit LSB
fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
}
@@ -548,6 +656,7 @@ public final class GifDecoder {
if (in.length < 6) { // Check first 6 bytes
throw new IOException("Image is truncated.");
}
// Use ASCII encoding for header - faster than default charset
img.header = new String(in, 0, 6);
if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
throw new IOException("Invalid GIF header.");
@@ -568,23 +677,28 @@ public final class GifDecoder {
fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
fr.clearCode = clearCode;
fr.endOfInfoCode = clearCode + 1;
// Pre-calculate image data size for better memory allocation
final int imgDataSize = readImgDataSize(in, i);
final byte[] imgData = new byte[imgDataSize + 2];
int imgDataPos = 0;
int subBlockSize = in[i] & 0xFF;
while (subBlockSize > 0) { // While block has data
try { // Next line may throw exception if sub-block size is fake
final int nextSubBlockSizePos = i + subBlockSize + 1;
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position
i = nextSubBlockSizePos; // Move to next sub-block size
subBlockSize = nextSubBlockSize;
} catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes
arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position
if (subBlockSize > 0) {
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position
}
i += subBlockSize + 1; // Move to next sub-block size
break;
}
@@ -608,7 +722,9 @@ public final class GifDecoder {
} catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes
imgDataPos += subBlockSize; // Move output data position
if (subBlockSize > 0) {
imgDataPos += subBlockSize; // Move output data position
}
break;
}
}
@@ -622,16 +738,16 @@ public final class GifDecoder {
* @return Index of the first byte after this block
*/
static int readImgDescr(final GifFrame fr, final byte[] in, int i) {
fr.x = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 1-2: left
fr.y = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 3-4: top
fr.w = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 5-6: width
fr.h = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 7-8: height
fr.x = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 1-2: left
fr.y = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 3-4: top
fr.w = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 5-6: width
fr.h = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 7-8: height
fr.wh = fr.w * fr.h;
final byte b = in[++i]; // Byte 9 is a packed byte
fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6
fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5
final int colTblSizePower = (b & 7) + 1; // Bits 2-0
final int packed = in[++i] & 0xFF; // Byte 9 is a packed byte
fr.hasLocColTbl = (packed & 0b10000000) != 0; // Bit 7
fr.interlaceFlag = (packed & 0b01000000) != 0; // Bit 6
fr.sortFlag = (packed & 0b00100000) != 0; // Bit 5
final int colTblSizePower = (packed & 7) + 1; // Bits 2-0
fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
return ++i;
}
@@ -642,15 +758,15 @@ public final class GifDecoder {
* @return Index of the first byte after this block.
*/
static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) {
img.w = in[i] & 0xFF | (in[i + 1] & 0xFF) << 8; // 16 bit, LSB 1st
img.h = in[i + 2] & 0xFF | (in[i + 3] & 0xFF) << 8; // 16 bit
img.w = (in[i] & 0xFF) | ((in[i + 1] & 0xFF) << 8); // 16 bit, LSB 1st
img.h = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // 16 bit
img.wh = img.w * img.h;
final byte b = in[i + 4]; // Byte 4 is a packed byte
img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4
final int packed = in[i + 4] & 0xFF; // Byte 4 is a packed byte
img.hasGlobColTbl = (packed & 0b10000000) != 0; // Bit 7
final int colResPower = ((packed & 0b01110000) >>> 4) + 1; // Bits 6-4
img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3
final int globColTblSizePower = (b & 7) + 1; // Bits 0-2
img.sortFlag = (packed & 0b00001000) != 0; // Bit 3
final int globColTblSizePower = (packed & 7) + 1; // Bits 0-2
img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
@@ -667,6 +783,7 @@ public final class GifDecoder {
int subBlockSize = in[i++] & 0xFF;
while (subBlockSize != 0 && i < in.length) {
i += subBlockSize;
if (i >= in.length) break; // Safety check
subBlockSize = in[i++] & 0xFF;
}
return i;
@@ -4,20 +4,30 @@ import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.util.Identifier;
import org.lwjgl.BufferUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.awt.image.DataBufferInt;
public class IdentifierUtils {
/**
* ORIGINAL METHOD - EXTREMELY SLOW - causes 10-20 second freezes
* This converts BufferedImage -> PNG bytes -> ByteBuffer -> NativeImage ->
* Texture
* This is completely unnecessary and wastes massive amounts of CPU time!
*/
public static void registerBufferedImageTexture(Identifier i, BufferedImage bi) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(bi, "png", baos);
// This is the performance killer:
// 1. Converts BufferedImage to PNG (expensive compression)
// 2. Creates ByteBuffer copy (memory allocation)
// 3. NativeImage.read() parses PNG back to raw pixels (expensive decompression)
// This is doing compression -> decompression for no reason!
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
javax.imageio.ImageIO.write(bi, "png", baos);
byte[] bytes = baos.toByteArray();
ByteBuffer bb = BufferUtils.createByteBuffer(bytes.length).put(bytes);
java.nio.ByteBuffer bb = org.lwjgl.BufferUtils.createByteBuffer(bytes.length).put(bytes);
bb.flip();
NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb));
SaturnClient.client.getTextureManager().registerTexture(i, nibt);
@@ -25,4 +35,64 @@ public class IdentifierUtils {
e.printStackTrace();
}
}
/**
* OPTIMIZED METHOD - Direct pixel conversion without unnecessary PNG
* encoding/decoding
* This should reduce loading time from 10-20 seconds to under 1 second
*/
public static void registerBufferedImageTextureFast(Identifier identifier, BufferedImage bufferedImage) {
try {
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
// Create NativeImage directly with the correct format
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
// Direct pixel copy - much faster than PNG conversion
if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
// Fast path for ARGB images - direct memory access
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixel = pixels[y * width + x];
nativeImage.setColorArgb(x, y, pixel);
}
}
} else {
// Fallback for other image types - still faster than PNG conversion
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = bufferedImage.getRGB(x, y);
nativeImage.setColorArgb(x, y, rgb);
}
}
}
// Register the texture
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
SaturnClient.client.execute(
() -> SaturnClient.client.getTextureManager().registerTexture(identifier, texture));
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register texture: " + identifier, e);
// Fallback to slow method if fast method fails
registerBufferedImageTexture(identifier, bufferedImage);
}
}
/**
* EVEN FASTER VERSION - Bulk texture registration
* Use this when registering many textures at once (like animated cloak frames)
*/
public static void registerBufferedImageTexturesBulk(java.util.Map<Identifier, BufferedImage> textures) {
long startTime = System.currentTimeMillis();
for (java.util.Map.Entry<Identifier, BufferedImage> entry : textures.entrySet()) {
registerBufferedImageTextureFast(entry.getKey(), entry.getValue());
}
long endTime = System.currentTimeMillis();
SaturnClient.LOGGER.info("Registered {} textures in {}ms", textures.size(), (endTime - startTime));
}
}
@@ -3,12 +3,17 @@ package org.saturnclient.saturnclient.event;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.option.KeyBinding;
// import net.minecraft.client.option.Perspective;
import net.minecraft.client.util.InputUtil;
// import org.saturnclient.modules.mods.FreeLook;
import org.saturnclient.saturnclient.menus.ModMenu;
// import org.saturnclient.saturnclient.menus.SaturnMenu;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.client.ServiceClient;
import org.saturnclient.saturnclient.config.Config;
import org.saturnclient.ui2.screens.EmoteWheel;
import org.saturnclient.ui2.screens.ShiftMenu;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import org.lwjgl.glfw.GLFW;
/**
@@ -21,55 +26,30 @@ public class KeyInputHandler {
private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT;
private static KeyBinding mainMenuKeyBinding;
// private static final String FREELOOK_KEY_ID = "key.saturnclient.freelook";
// private static final int FREELOOK_KEY = GLFW.GLFW_KEY_M;
// private static KeyBinding freelookKeyBinding;
/**
* Registers all keybindings and their associated event handlers.
* Should be called during mod initialization.
*/
public static void register() {
// Register the main menu key binding
mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
MAIN_MENU_KEY_ID,
InputUtil.Type.KEYSYM,
DEFAULT_MENU_KEY,
KEY_CATEGORY));
// freelookKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
// FREELOOK_KEY_ID,
// InputUtil.Type.KEYSYM,
// FREELOOK_KEY,
// KEY_CATEGORY));
// Register the event handler for the main menu key
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (mainMenuKeyBinding.wasPressed()) {
client.setScreen(new ModMenu());
client.setScreen(new ShiftMenu());
}
if (Config.openEmoteWheel.wasKeyPressed() && client.currentScreen == null && !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) {
client.setScreen(new EmoteWheel());
}
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
animationStack.removeLayer(1000);
ServiceClient.emote("");
}
}
// else if (freelookKeyBinding.wasPressed() && FreeLook.enabled.value && FreeLook.toggle.value) {
// FreeLook.isFreeLooking = !FreeLook.isFreeLooking;
// if (FreeLook.isFreeLooking) {
// FreeLook.lastPerspective = client.options.getPerspective();
// client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
// } else if (FreeLook.lastPerspective != null) {
// client.options.setPerspective(FreeLook.lastPerspective);
// FreeLook.lastPerspective = null;
// }
// } else if (FreeLook.enabled.value && !FreeLook.toggle.value) {
// if (freelookKeyBinding.isPressed()) {
// if (!FreeLook.isFreeLooking) {
// FreeLook.lastPerspective = client.options.getPerspective();
// client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
// FreeLook.isFreeLooking = true;
// }
// } else if (FreeLook.isFreeLooking && FreeLook.lastPerspective != null) {
// client.options.setPerspective(FreeLook.lastPerspective);
// FreeLook.isFreeLooking = false;
// FreeLook.lastPerspective = null;
// }
// }
});
}
}
@@ -1,158 +0,0 @@
package org.saturnclient.saturnclient.menus;
import net.minecraft.text.Text;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.components.SkinPreview;
import org.saturnclient.ui.widgets.SaturnClickableImage;
import org.saturnclient.ui.widgets.SaturnClickableSprite;
import org.saturnclient.ui.widgets.SaturnImage;
import org.saturnclient.ui.widgets.SaturnInputBox;
import org.saturnclient.ui.widgets.SaturnScroll;
import org.saturnclient.ui.widgets.SaturnSprite;
public class CloaksMenu extends SaturnUi {
private SaturnScroll scroll;
private Property<String> search = new Property<>("");
public CloaksMenu() {
super(Text.of("Cloaks Menu"));
}
@Override
protected void init() {
int rectWidth = 330;
int rectHeight = 228;
int rectX = (width - rectWidth) / 2;
int rectY = (height - rectHeight + 15) / 2;
draw(
new SaturnSprite(Textures.SETTINGS_BG)
.setX(rectX)
.setY(rectY)
.setWidth(rectWidth)
.setHeight(rectHeight)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabsX = (width - 43) / 2;
int tabsY = rectY - 17;
draw(
new SaturnSprite(Textures.RECT)
.setX(tabsX)
.setY(tabsY)
.setWidth(43)
.setHeight(15)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnInputBox(search, rectX + rectWidth - 72, tabsY, 70).onUpdate(() -> {
scroll.clear();
drawItems(rectX, rectY, rectWidth, rectHeight);
})
.setHeight(15)
.setWidth(72)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabSize = 9;
tabsX = (width - tabSize) / 2;
draw(
new SaturnClickableImage(Textures.HAT, () -> {
client.setScreen(new HatsMenu());
})
.setX(tabsX)
.setY(tabsY + 4)
.setWidth(tabSize)
.setHeight(7)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.CLOAK, () -> {
})
.setSelected(true)
.setX(tabsX - 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.SEARCH, () -> {
System.out.println("Pressed");
})
.setX(tabsX + 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
scroll = new SaturnScroll();
drawItems(rectX, rectY, rectWidth, rectHeight);
draw(scroll
.setX(rectX + 17)
.setY(rectY + 10)
.setWidth(rectWidth - 17)
.setHeight(rectHeight - 10)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(new SkinPreview()
.setScale(3.0f)
.setX((rectX + rectWidth) - 180)
.setY(rectY + 8)
.setAnimations(SaturnClientConfig.getAnimations()));
super.init();
}
private void drawItems(int rectX, int rectY, int rectWidth, int rectHeight) {
int cloakWidth = 28;
int cloakHeight = 61;
int modX = 0;
int modY = 0;
int col = 0;
String[] searchTokens = search.value.toLowerCase().split("\\ ");
outer: for (String cloak : Cloaks.availableCloaks) {
for (String searchToken : searchTokens) {
if (!cloak.contains(searchToken)) {
continue outer;
}
}
scroll.draw(
new SaturnClickableSprite(Textures.BUTTON_BORDER, () -> {
Cloaks.setCloak(Auth.uuid, cloak);
})
.setX(modX)
.setY(modY)
.setWidth(cloakWidth + 6)
.setHeight(cloakHeight + 6));
scroll.draw(
new SaturnImage(Textures.getCloakPreview(cloak))
.setX(modX + 3)
.setY(modY + 3)
.setWidth(cloakWidth)
.setHeight(cloakHeight));
col++;
if (col < 4) {
modX += cloakWidth + 21;
} else {
modX = 0;
modY += cloakHeight + 20;
col = 0;
}
}
}
}
@@ -1,234 +0,0 @@
package org.saturnclient.saturnclient.menus;
import java.util.Map;
import net.minecraft.text.Text;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.components.SaturnFloat;
import org.saturnclient.ui.components.SaturnHex;
import org.saturnclient.ui.components.SaturnInteger;
import org.saturnclient.ui.components.SaturnString;
import org.saturnclient.ui.components.SaturnToggle;
import org.saturnclient.ui.widgets.SaturnImageButton;
import org.saturnclient.ui.widgets.SaturnScroll;
import org.saturnclient.ui.widgets.SaturnSprite;
import org.saturnclient.ui.widgets.SaturnText;
public class ConfigEditor extends SaturnUi {
private ConfigManager config;
public ConfigEditor(ConfigManager config) {
super(Text.of("Config editor"));
this.config = config;
}
@Override
protected void init() {
int rectWidth = 330;
int rectHeight = 228;
int rectX = (width - rectWidth) / 2;
int rectY = (height - rectHeight + 15) / 2;
draw(
new SaturnSprite(Textures.SETTINGS_BG)
.setX(rectX)
.setY(rectY)
.setWidth(rectWidth)
.setHeight(rectHeight)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnImageButton(Textures.CLOSE, 9, 9, () -> {
this.close();
})
.setX(rectX)
.setY(rectY - 15)
.setWidth(13)
.setHeight(13)
.setAnimations(SaturnClientConfig.getAnimations()));
SaturnScroll configScroll = new SaturnScroll();
int row = 0;
int col = 0;
drawProperties(configScroll, config.getProperties(), rectWidth, row, col);
draw(
configScroll
.setX(rectX + 17)
.setY(rectY + 10)
.setWidth(rectWidth - 17)
.setHeight(rectHeight - 10)
.setAnimations(SaturnClientConfig.getAnimations()));
super.init();
}
@SuppressWarnings("unchecked")
private int drawProperties(SaturnScroll configScroll, Map<String, Property<?>> properties, int rectWidth,
int row, int col) {
int totalRows = row;
for (Map.Entry<String, Property<?>> propEntry : properties.entrySet()) {
Property<?> prop = propEntry.getValue();
String propName = propEntry.getKey();
boolean full = isFull(prop);
if (full && col > 0) {
col = 0;
totalRows++;
}
int modX = ((rectWidth / 2) - 17) * col;
int modY = 14 * totalRows;
switch (prop.getType()) {
case BOOLEAN:
configScroll.draw(
new SaturnToggle((Property<Boolean>) prop)
.setX(modX)
.setY(modY));
configScroll.draw(
new SaturnText(
propName.substring(0, 1).toUpperCase() +
propName.substring(1))
.setX(modX + 18)
.setY(modY)
.setScale(0.8f));
break;
case INTEGER:
configScroll.draw(
new SaturnText(
propName.substring(0, 1).toUpperCase() +
propName.substring(1))
.setX(modX)
.setY(modY)
.setScale(0.8f));
configScroll.draw(
new SaturnInteger(
(Property<Integer>) prop,
(rectWidth / 2) - 17,
modY,
70));
configScroll.draw(resetButton(prop, modX, modY));
break;
case FLOAT:
configScroll.draw(
new SaturnText(
propName.substring(0, 1).toUpperCase() +
propName.substring(1))
.setX(modX)
.setY(modY)
.setScale(0.8f));
configScroll.draw(
new SaturnFloat(
(Property<Float>) prop,
(rectWidth / 2) - 17,
modY,
70));
configScroll.draw(resetButton(prop, modX, modY));
break;
case STRING:
configScroll.draw(
new SaturnText(
propName.substring(0, 1).toUpperCase() +
propName.substring(1))
.setX(modX)
.setY(modY)
.setScale(0.8f));
configScroll.draw(
new SaturnString(
(Property<String>) prop,
(rectWidth / 2) - 17,
modY,
70));
configScroll.draw(resetButton(prop, modX, modY));
break;
case HEX:
configScroll.draw(
new SaturnText(
propName.substring(0, 1).toUpperCase() +
propName.substring(1))
.setX(modX)
.setY(modY)
.setScale(0.8f));
configScroll.draw(
new SaturnHex(
(Property<Integer>) prop,
(rectWidth / 2) - 17,
modY,
70));
configScroll.draw(resetButton(prop, modX, modY));
break;
case NAMESPACE:
configScroll.draw(
new SaturnText(
propName.substring(0, 1).toUpperCase() +
propName.substring(1))
.setX(modX)
.setY(modY)
.setScale(0.8f));
// Recursively draw nested properties and update total rows
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) prop.value;
totalRows = drawProperties(configScroll, nestedProperties, rectWidth,
totalRows + 1, 0);
}
if (full) {
totalRows++;
} else {
col++;
if (col > 1) {
col = 0;
totalRows++;
}
}
}
return totalRows;
}
public static boolean isFull(Property<?> prop) {
switch (prop.getType()) {
case BOOLEAN:
return false;
default:
return true;
}
}
@Override
public void close() {
ConfigManager.save();
super.close();
}
public SaturnWidget resetButton(Property<?> prop, int modX, int modY) {
int size = textRenderer.fontHeight - 1;
return new SaturnImageButton(
Textures.RESET,
size,
size,
() -> {
prop.reset();
})
.setX(modX + 209)
.setY(modY)
.setWidth(size + 3)
.setHeight(size + 3);
}
}
@@ -1,133 +0,0 @@
package org.saturnclient.saturnclient.menus;
import net.minecraft.text.Text;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.components.SkinPreview;
import org.saturnclient.ui.widgets.SaturnClickableImage;
import org.saturnclient.ui.widgets.SaturnClickableSprite;
import org.saturnclient.ui.widgets.SaturnImage;
import org.saturnclient.ui.widgets.SaturnScroll;
import org.saturnclient.ui.widgets.SaturnSprite;
public class HatsMenu extends SaturnUi {
public HatsMenu() {
super(Text.of("Hats Menu"));
}
@Override
protected void init() {
int rectWidth = 330;
int rectHeight = 228;
int rectX = (width - rectWidth) / 2;
int rectY = (height - rectHeight + 15) / 2;
draw(
new SaturnSprite(Textures.SETTINGS_BG)
.setX(rectX)
.setY(rectY)
.setWidth(rectWidth)
.setHeight(rectHeight)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabsX = (width - 43) / 2;
int tabsY = rectY - 17;
draw(
new SaturnSprite(Textures.RECT)
.setX(tabsX)
.setY(tabsY)
.setWidth(43)
.setHeight(15)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabSize = 9;
tabsX = (width - tabSize) / 2;
draw(
new SaturnClickableImage(Textures.HAT, () -> {
client.setScreen(new HatsMenu());
})
.setSelected(true)
.setX(tabsX)
.setY(tabsY + 4)
.setWidth(tabSize)
.setHeight(7)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.CLOAK, () -> {
client.setScreen(new CloaksMenu());
})
.setX(tabsX - 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.SEARCH, () -> {
System.out.println("Pressed");
})
.setX(tabsX + 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
int hatSize = 28;
int modX = 0;
int modY = 0;
int col = 0;
SaturnScroll modsScroll = new SaturnScroll();
for (String hat : Hats.availableHats) {
modsScroll.draw(
new SaturnClickableSprite(Textures.BUTTON_BORDER, () -> {
Hats.setHat(Auth.uuid, hat);
})
.setX(modX)
.setY(modY)
.setWidth(hatSize + 6)
.setHeight(hatSize + 6));
modsScroll.draw(
new SaturnImage(Textures.getHatPreview(hat))
.setX(modX + 3)
.setY(modY + 3)
.setWidth(hatSize)
.setHeight(hatSize));
col++;
if (col < 4) {
modX += hatSize + 21;
} else {
modX = 0;
modY += hatSize + 20;
col = 0;
}
}
draw(
modsScroll
.setX(rectX + 17)
.setY(rectY + 10)
.setWidth(rectWidth - 17)
.setHeight(rectHeight - 10)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(new SkinPreview()
.setScale(3.0f)
.setX((rectX + rectWidth) - 180)
.setY(rectY + 8)
.setAnimations(SaturnClientConfig.getAnimations()));
super.init();
}
}
@@ -9,8 +9,10 @@ import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.ModManager;
import org.saturnclient.modules.Module;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.SaturnScreen;
import java.util.ArrayList;
import java.util.List;
@@ -24,7 +26,7 @@ public class HudEditor extends Screen {
public HudEditor() {
super(Text.literal("HUD Editor"));
for (Module m : ModManager.MODS) {
for (Module m : ModManager.ENABLED_MODS) {
if (m.isEnabled() && m instanceof HudMod) {
hudMods.add((HudMod) m);
}
@@ -90,9 +92,11 @@ public class HudEditor extends Screen {
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
if (client.world == null && client.getCurrentServerEntry() == null) {
SaturnUi.ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, 1.0F, delta);
SaturnScreen.ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, 1.0F, delta);
}
RenderScope scope = new RenderScope(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers());
for (HudMod mod : hudMods) {
ModDimensions dim = mod.getDimensions();
MatrixStack matrices = context.getMatrices();
@@ -102,7 +106,7 @@ public class HudEditor extends Screen {
matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
// Render the mod
mod.renderDummy(context);
mod.renderDummy(scope);
matrices.pop();
@@ -1,147 +0,0 @@
package org.saturnclient.saturnclient.menus;
import net.minecraft.text.Text;
import org.saturnclient.modules.ModManager;
import org.saturnclient.modules.Module;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.components.SaturnModComp;
import org.saturnclient.ui.widgets.SaturnClickableImage;
import org.saturnclient.ui.widgets.SaturnInputBox;
import org.saturnclient.ui.widgets.SaturnScroll;
import org.saturnclient.ui.widgets.SaturnSprite;
public class ModMenu extends SaturnUi {
private SaturnScroll scroll;
private Property<String> search = new Property<>("");
public ModMenu() {
super(Text.literal("Mods"));
}
@Override
protected void init() {
int rectWidth = 330;
int rectHeight = 228;
int rectX = (width - rectWidth) / 2;
int rectY = (height - rectHeight + 15) / 2;
draw(
new SaturnSprite(Textures.SETTINGS_BG)
.setX(rectX)
.setY(rectY)
.setWidth(rectWidth)
.setHeight(rectHeight)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabsX = (width - 43) / 2;
int tabsY = rectY - 17;
draw(
new SaturnSprite(Textures.RECT)
.setX(tabsX)
.setY(tabsY)
.setWidth(43)
.setHeight(15)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnInputBox(search, rectX + rectWidth - 72, tabsY, 70).onUpdate(() -> {
scroll.clear();
drawItems(rectX, rectY, rectWidth, rectHeight);
})
.setHeight(15)
.setWidth(72)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabSize = 9;
tabsX = (width - tabSize) / 2;
draw(
new SaturnClickableImage(Textures.SETTINGS, () -> {
client.setScreen(new SaturnConfigEditor());
})
.setX(tabsX)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.MODS_TAB, () -> {
})
.setSelected(true)
.setX(tabsX - 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.SEARCH, () -> {
System.out.println("Pressed");
})
.setX(tabsX + 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
scroll = new SaturnScroll();
drawItems(rectX, rectY, rectWidth, rectHeight);
draw(scroll
.setX(rectX + 17)
.setY(rectY + 10)
.setWidth(rectWidth - 17)
.setHeight(rectHeight - 20)
.setAnimations(SaturnClientConfig.getAnimations()));
super.init();
}
@Override
public void close() {
ConfigManager.save();
super.close();
}
private void drawItems(int rectX, int rectY, int rectWidth, int rectHeight) {
int modX = 0;
int modY = 0;
int col = 0;
String[] searchTokens = search.value.toLowerCase().split("\\ ");
outer: for (Module mod : ModManager.MODS) {
String modName = mod.getName().toLowerCase();
for (String searchToken : searchTokens) {
if (!modName.contains(searchToken)) {
continue outer;
}
}
scroll.draw(
new SaturnModComp(mod)
.setX(modX)
.setY(modY)
.setWidth(86)
.setHeight(20));
col++;
if (col < 3) {
modX += 86 + 18;
} else {
modX = 0;
modY += 20 + 20;
col = 0;
}
}
}
}
@@ -1,64 +0,0 @@
package org.saturnclient.saturnclient.menus;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.widgets.SaturnClickableImage;
import org.saturnclient.ui.widgets.SaturnSprite;
public class SaturnConfigEditor extends ConfigEditor {
public SaturnConfigEditor() {
super(SaturnClientConfig.config);
}
@Override
protected void init() {
int rectHeight = 228;
int rectY = (height - rectHeight + 15) / 2;
int tabsX = (width - 43) / 2;
int tabsY = rectY - 17;
draw(
new SaturnSprite(Textures.RECT)
.setX(tabsX)
.setY(tabsY)
.setWidth(43)
.setHeight(15)
.setAnimations(SaturnClientConfig.getAnimations()));
int tabSize = 9;
tabsX = (width - tabSize) / 2;
draw(
new SaturnClickableImage(Textures.SETTINGS, () -> {
})
.setSelected(true)
.setX(tabsX)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.MODS_TAB, () -> {
client.setScreen(new ModMenu());
})
.setX(tabsX - 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
draw(
new SaturnClickableImage(Textures.SEARCH, () -> {
System.out.println("Pressed");
})
.setX(tabsX + 12)
.setY(tabsY + 3)
.setWidth(tabSize)
.setHeight(tabSize)
.setAnimations(SaturnClientConfig.getAnimations()));
super.init();
}
}
@@ -1,13 +0,0 @@
package org.saturnclient.saturnclient.menus;
import org.saturnclient.ui2.SaturnScreen;
public class SaturnMenu extends SaturnScreen {
public SaturnMenu() {
super("Saturn Client");
}
@Override
public void ui() {
}
}
@@ -1,78 +0,0 @@
package org.saturnclient.saturnclient.menus;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.Textures;
import org.saturnclient.ui.animations.FadeIn;
import org.saturnclient.ui.animations.Slide;
import org.saturnclient.ui.widgets.SaturnButton;
import org.saturnclient.ui.widgets.SaturnImage;
import org.saturnclient.ui2.screens.ShiftMenu;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.option.OptionsScreen;
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
import net.minecraft.text.Text;
public class SaturnTitleScreen extends SaturnUi {
public SaturnTitleScreen() {
super(Text.literal("Title Screen"));
}
@Override
protected void init() {
blur = false;
int logoSize = 49;
int bX = (width - 150) / 2;
int bY = height / 2 - logoSize - 27;
draw(
new SaturnImage(SaturnClientConfig.getLogo())
.setX((width - logoSize) / 2)
.setY(bY)
.setWidth(logoSize)
.setHeight(logoSize)
.setAnimations(new Slide(3, 14)));
bY += 44;
draw(
new SaturnImage(Textures.LOGO_TEXT)
.setX((width - logoSize) / 2)
.setY(bY)
.setWidth(logoSize)
.setHeight(8)
.setAnimations(new FadeIn(3)));
bY += 15;
draw(new SaturnButton("SINGLEPLAYER", () -> {
client.setScreen(new SelectWorldScreen(new SaturnTitleScreen()));
}).setBold(true).setWidth(210).setHeight(26).setX(bX).setY(bY).setScale(0.69f));
bY += 20;
draw(new SaturnButton("MULTIPLAYER", () -> {
client.setScreen(new MultiplayerScreen(new SaturnTitleScreen()));
}).setBold(true).setWidth(210).setHeight(26).setX(bX).setY(bY).setScale(0.69f));
bY += 20;
draw(new SaturnButton("OPTIONS", () -> {
client.setScreen(new OptionsScreen(new SaturnTitleScreen(), this.client.options));
}).setBold(true).setWidth(103).setHeight(26).setX(bX).setY(bY).setScale(0.69f));
draw(new SaturnButton("QUIT", () -> {
client.scheduleStop();
}).setBold(true).setWidth(103).setHeight(26).setX(bX + 74).setY(bY).setScale(0.69f));
bY += 20;
draw(new SaturnButton("SATURN OPTIONS", () -> {
client.setScreen(new ShiftMenu());
}).setBold(true).setWidth(210).setHeight(26).setX(bX).setY(bY).setScale(0.69f));
super.init();
}
}
@@ -14,8 +14,11 @@ import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.math.Vec3d;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.client.player.Roles;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import java.util.UUID;
import org.joml.Matrix4f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -37,12 +40,12 @@ public abstract class NameTagMixin<S extends EntityRenderState> {
private void onRenderLabelIfPresent(S state, Text text, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light, CallbackInfo ci) {
String uuid = isSaturn(state);
UUID uuid = isSaturn(state);
if (uuid != null) {
MutableText iconText = Text.literal(SaturnClientConfig.getSaturnIndicator())
.styled(style -> style.withColor(SaturnClientConfig.getIconColor(uuid)));
MutableText iconText = Text.literal(Roles.getSaturnIndicator())
.styled(style -> style.withColor(Roles.getIconColor(uuid)));
Text nameText = text.copy().styled(style -> style.withColor(Formatting.WHITE));
renderLabel(state, iconText.append(nameText), matrices,
@@ -52,12 +55,12 @@ public abstract class NameTagMixin<S extends EntityRenderState> {
}
}
private String isSaturn(S state) {
private UUID isSaturn(S state) {
if (state instanceof PlayerEntityRenderState) {
String name = ((PlayerEntityRenderState) state).name;
String uuid = Auth.playerNames.get(name);
if (Auth.players.containsKey(uuid)) {
return uuid;
SaturnPlayer player = SaturnPlayer.get(name);
if (player != null) {
return player.uuid;
}
}
@@ -25,6 +25,6 @@ public abstract class PlayerEntityRendererMixin
@Inject(method = "<init>", at = @At("TAIL"))
private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) {
this.addFeature(new HatFeatureRenderer(this));
this.addFeature(new CloakFeatureRenderer(this, ctx.getEntityModels(), ctx.getEquipmentModelLoader()));
this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader()));
}
}
@@ -1,33 +0,0 @@
package org.saturnclient.saturnclient.mixin;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.saturnclient.auth.Auth;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.client.gui.hud.MessageIndicator;
import net.minecraft.network.message.MessageSignatureData;
import net.minecraft.text.Text;
@Mixin(ChatHud.class)
public class SaturnReload {
@Inject(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V", at = @At("HEAD"), cancellable = true)
public void onAddMessage(Text message, @Nullable MessageSignatureData signatureData,
@Nullable MessageIndicator indicator, CallbackInfo ci) {
String msg = message.toString();
if (msg.contains("$SATURN_RELOAD")) {
for (Map.Entry<String, String> player : Auth.playerNames.entrySet()) {
String name = player.getKey();
if (msg.contains(name)) {
Auth.player(name, player.getValue());
}
ci.cancel();
}
}
}
}
@@ -0,0 +1,26 @@
package org.saturnclient.saturnclient.mixin;
import org.saturnclient.saturnclient.config.Config;
import org.saturnclient.ui2.screens.TitleMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.TitleScreen;
@Mixin(MinecraftClient.class)
public class SetScreenMixin {
@ModifyVariable(method = "setScreen", at = @At("HEAD"), argsOnly = true)
private Screen replaceTitleScreen(Screen screen) {
if (Config.saturnTitleScreen.value) {
if (screen instanceof TitleScreen)
return new TitleMenu();
if (screen == null && ((MinecraftClient) (Object) this).world == null)
return new TitleMenu();
}
return screen;
}
}
@@ -0,0 +1,160 @@
package org.saturnclient.saturnclient.mixin;
import java.util.Optional;
import java.util.function.Consumer;
import org.saturnclient.saturnclient.config.Theme;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Textures;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Final;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.SplashOverlay;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.resource.ResourceReload;
import net.minecraft.util.Util;
import net.minecraft.util.math.MathHelper;
@Mixin(SplashOverlay.class)
public abstract class SplashOverlayMixin {
@Shadow
private MinecraftClient client;
@Shadow
private ResourceReload reload;
@Shadow
private long reloadCompleteTime;
@Shadow
private long reloadStartTime;
@Shadow @Final
private boolean reloading;
@Shadow @Final
private Consumer<Optional<Throwable>> exceptionHandler;
@Shadow
private float progress;
/**
* @author SaturnClient
* @reason Replace vanilla splash rendering completely but keep exit logic
*/
@Overwrite
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
int screenWidth = context.getScaledWindowWidth();
int screenHeight = context.getScaledWindowHeight();
long currentTime = Util.getMeasuringTimeMs();
// Initialize reload start time if not set
if (reloadStartTime == -1L) {
reloadStartTime = currentTime;
}
// Compute fade-out progress after reload completes
float fadeOutProgress = reloadCompleteTime > -1L
? (float) (currentTime - reloadCompleteTime) / 1000f
: -1f;
int alpha = fadeOutProgress >= 0f
? MathHelper.ceil((1.0f - MathHelper.clamp(fadeOutProgress - 1f, 0f, 1f)) * 255f)
: 255;
// Compute fade-in progress while reloading
float reloadProgress = reloadStartTime > -1L
? (float) (currentTime - reloadStartTime) / 500f
: -1f;
// Get render scope for better UI
RenderScope scope = new RenderScope(context);
scope.setRenderLayer(RenderLayer::getGuiTextured);
// Draw background overlay
if (fadeOutProgress >= 1.0F) {
renderCurrentScreen(context, mouseX, mouseY, delta);
int overlayAlpha = computeOverlayAlpha(fadeOutProgress - 1f);
scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
Theme.withAlpha(overlayAlpha, 0xFFFFFFFF));
} else if (reloading) {
if (reloadProgress < 1.0F) {
renderCurrentScreen(context, mouseX, mouseY, delta);
}
scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
Theme.withAlpha(alpha, 0xFFFFFFFF));
} else {
scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
Theme.withAlpha(alpha, 0xFFFFFFFF));
}
float u = this.reload.getProgress();
this.progress = MathHelper.clamp(this.progress * 0.95F + u * 0.050000012F, 0.0F, 1.0F);
// Draw central logo
drawLogo(scope, screenWidth, screenHeight, alpha);
renderProgressBar(scope, context, screenWidth, screenHeight, alpha);
// Remove overlay after fade-out completes
if (fadeOutProgress >= 2.0F) {
client.setOverlay(null);
}
// Handle reload completion and exceptions
handleReloadCompletion(context);
}
private void renderCurrentScreen(DrawContext context, int mouseX, int mouseY, float delta) {
if (client.currentScreen != null) {
client.currentScreen.render(context, mouseX, mouseY, delta);
}
}
/** Draw the main logo centered */
private void drawLogo(RenderScope scope, int screenWidth, int screenHeight, int alpha) {
scope.drawTexture(Textures.LOGO, (screenWidth - 60) / 2, ((screenHeight - 60) / 2) - 20, 0, 0, 60, 60,
Theme.withAlpha(alpha, 0xFFFFFFFF));
}
private void renderProgressBar(RenderScope scope, DrawContext context, int screenWidth, int screenHeight,
int alpha) {
int progressY = ((screenHeight - 5) / 2) + 25;
context.drawBorder((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
int progressWidth = Math.min((int) (progress * 120), 120);
int progressX = (screenWidth - 120) / 2;
context.fill(progressX, progressY, progressX + progressWidth, progressY + 5,
Theme.withAlpha(alpha, 0xFFFFFFFF));
}
/** Compute overlay alpha from fade progress */
private int computeOverlayAlpha(float progress) {
return MathHelper.ceil((1.0F - MathHelper.clamp(progress, 0.0F, 1.0F)) * 255.0F);
}
/** Handle reload completion logic */
private void handleReloadCompletion(DrawContext context) {
float reloadProgress = reloadStartTime > -1L ? (float) (Util.getMeasuringTimeMs() - reloadStartTime) / 500f
: -1f;
if (reloadCompleteTime == -1L && reload.isComplete() && (!reloading || reloadProgress >= 2.0F)) {
try {
reload.throwException();
exceptionHandler.accept(Optional.empty());
} catch (Throwable ex) {
exceptionHandler.accept(Optional.of(ex));
}
reloadCompleteTime = Util.getMeasuringTimeMs();
if (client.currentScreen != null) {
client.currentScreen.init(client, context.getScaledWindowWidth(), context.getScaledWindowHeight());
}
}
}
}
@@ -1,13 +1,14 @@
package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.client.player.Roles;
import org.saturnclient.saturnclient.client.player.SaturnPlayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@@ -30,13 +31,13 @@ public abstract class TabListMixin {
public Text getDisplayName() {
GameProfile profile = ((PlayerListEntry) (Object) this).getProfile();
String name = profile.getName();
String uuid = profile.getId().toString().replaceAll("-", "");
UUID uuid = profile.getId();
MutableText iconText = Text.literal(SaturnClientConfig.getSaturnIndicator())
.styled(style -> style.withColor(SaturnClientConfig.getIconColor(uuid)));
Text nameText = Text.literal(name).styled(style -> style.withColor(Formatting.WHITE));
return Auth.players.containsKey(uuid) ? iconText.append(nameText) : Text.literal(name);
if (SaturnPlayer.get(uuid) != null) {
return Text.literal(Roles.getSaturnIndicator())
.styled(style -> style.withColor(Roles.getIconColor(uuid)))
.append(Text.literal(name).styled(style -> style.withColor(Formatting.WHITE)));
}
return Text.literal(name);
}
}
@@ -1,13 +0,0 @@
package org.saturnclient.ui;
public class SaturnAnimation {
public int delay = 0;
public int lastTick = 0;
public void init(SaturnWidget widget) {}
public boolean run(SaturnWidget widget, int tick) {
return true;
}
}
@@ -1,322 +0,0 @@
package org.saturnclient.ui;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.PostEffectProcessor;
import net.minecraft.client.gui.CubeMapRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.RotatingCubeMapRenderer;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.render.DefaultFramebufferSet;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.Pool;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.MutableText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.lwjgl.glfw.GLFW;
public class SaturnUi extends Screen {
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final Pool pool = new Pool(3);
private float f = 0.0f;
public boolean blur = true;
public boolean titleBackground = false;
public List<SaturnWidget> widgets = new ArrayList<>();
private int tick = 0;
protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(
Identifier.of("saturnclient", "textures/gui/title/background/panorama"));
public static final RotatingCubeMapRenderer ROTATING_PANORAMA_RENDERER;
static {
ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER);
}
public static MutableText text(String text) {
return Text.literal(text).setStyle(
Style.EMPTY.withFont(Identifier.of("saturnclient:panton")));
}
public static MutableText textBold(String text) {
return Text.literal(text).setStyle(
Style.EMPTY.withFont(Identifier.of("saturnclient:panton_bold")));
}
public static void drawHighResTexture(DrawContext context, Identifier t, int x, int y, int w, int h) {
MatrixStack matricies = context.getMatrices();
matricies.push();
matricies.translate(x, y, 0);
matricies.scale(.25f, .25f, 1.0f);
context.drawTexture(RenderLayer::getGuiTextured, t, 0, 0, 0, 0,
w * 4, h * 4, w * 4, h * 4);
matricies.pop();
}
public static void drawHighResTexture(DrawContext context, Identifier t, int x, int y, int w, int h, int c) {
MatrixStack matricies = context.getMatrices();
matricies.push();
matricies.translate(x, y, 0);
matricies.scale(.25f, .25f, 1.0f);
context.drawTexture(RenderLayer::getGuiTextured, t, 0, 0, 0, 0,
w * 4, h * 4, w * 4, h * 4, c);
matricies.pop();
}
public static void drawHighResGuiTexture(DrawContext context, Identifier t, int x, int y, int w, int h, int c) {
MatrixStack matricies = context.getMatrices();
matricies.push();
matricies.translate(x, y, 0);
matricies.scale(.25f, .25f, 1.0f);
context.drawGuiTexture(
RenderLayer::getGuiTextured,
t,
0,
0,
w * 4,
h * 4,
c);
matricies.pop();
}
public static int getAlpha(int color, float alpha) {
return ((int) (alpha * 255) << 24) |
(color & 0x00FFFFFF);
}
public SaturnUi(Text title) {
super(title);
}
@Override
protected void init() {
scheduler.scheduleAtFixedRate(
this::saturnTick,
0,
7,
TimeUnit.MILLISECONDS);
super.init();
}
public void draw(SaturnWidget widget) {
synchronized (widgets) {
widget.init();
widgets.add(widget);
}
}
public void refresh() {
synchronized (widgets) {
widgets.clear();
f = 0.0f;
tick = 0;
init();
}
}
@SuppressWarnings("deprecation")
@Override
public void render(
DrawContext context,
int mouseX,
int mouseY,
float delta) {
if (client.world == null && client.getCurrentServerEntry() == null) {
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, 1.0F, delta);
}
// Blur
if (!(f < 1.0F) && blur) {
PostEffectProcessor postEffectProcessor = this.client.getShaderLoader().loadPostEffect(
Identifier.ofVanilla("blur"),
DefaultFramebufferSet.MAIN_ONLY);
if (postEffectProcessor != null) {
postEffectProcessor.setUniforms("Radius", f);
postEffectProcessor.render(this.client.getFramebuffer(), this.pool);
}
}
for (SaturnWidget widget : new ArrayList<>(widgets)) {
if (!widget.visible)
continue;
double adjustedMouseX = mouseX - widget.x;
double adjustedMouseY = mouseY - widget.y;
boolean isMouseInside = adjustedMouseX >= 0 &&
adjustedMouseX <= (widget.width * widget.scale) &&
adjustedMouseY >= 0 &&
adjustedMouseY <= (widget.height * widget.scale);
MatrixStack matrices = context.getMatrices();
matrices.push();
matrices.translate(widget.x, widget.y, 0);
matrices.scale(widget.scale, widget.scale, 1.0f);
widget.render(
context,
isMouseInside,
(int) (adjustedMouseX / widget.scale),
(int) (adjustedMouseY / widget.scale));
matrices.pop();
}
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button == 0) {
for (SaturnWidget widget : new ArrayList<>(widgets)) {
widget.focused = false;
if (!widget.visible)
continue;
double adjustedMouseX = mouseX - widget.x;
double adjustedMouseY = mouseY - widget.y;
boolean isMouseInside = adjustedMouseX >= 0 &&
adjustedMouseX <= (widget.width * widget.scale) &&
adjustedMouseY >= 0 &&
adjustedMouseY <= (widget.height * widget.scale);
if (isMouseInside) {
widget.focused = true;
widget.click(
(int) (adjustedMouseX / widget.scale),
(int) (adjustedMouseY / widget.scale));
}
}
}
return false;
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
for (SaturnWidget widget : new ArrayList<>(widgets)) {
if (!widget.visible && !widget.focused)
continue;
widget.keyPressed(keyCode, scanCode, modifiers);
char typedChar = getCharFromKey(keyCode, modifiers);
if (typedChar != '\0') {
widget.charTyped(typedChar);
}
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public boolean mouseScrolled(
double mouseX,
double mouseY,
double horizontalAmount,
double verticalAmount) {
for (SaturnWidget widget : new ArrayList<>(widgets)) {
double adjustedMouseX = mouseX - widget.x;
double adjustedMouseY = mouseY - widget.y;
boolean isMouseInside = adjustedMouseX >= 0 &&
adjustedMouseX <= (widget.width * widget.scale) &&
adjustedMouseY >= 0 &&
adjustedMouseY <= (widget.height * widget.scale);
if (isMouseInside) {
widget.mouseScrolled(
(int) (adjustedMouseX / widget.scale),
(int) (adjustedMouseY / widget.scale),
horizontalAmount,
verticalAmount);
}
}
return super.mouseScrolled(
mouseX,
mouseY,
horizontalAmount,
verticalAmount);
}
private char getCharFromKey(int keyCode, int modifiers) {
boolean shift = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0;
// A-Z
if (keyCode >= GLFW.GLFW_KEY_A && keyCode <= GLFW.GLFW_KEY_Z) {
return (char) (shift
? 'A' + (keyCode - GLFW.GLFW_KEY_A)
: 'a' + (keyCode - GLFW.GLFW_KEY_A));
}
// 0-9
if (keyCode >= GLFW.GLFW_KEY_0 && keyCode <= GLFW.GLFW_KEY_9) {
return shift
? ")!@#$%^&*(".charAt(keyCode - GLFW.GLFW_KEY_0)
: (char) ('0' + (keyCode - GLFW.GLFW_KEY_0));
}
// Special characters
return switch (keyCode) {
case GLFW.GLFW_KEY_SPACE -> ' ';
case GLFW.GLFW_KEY_PERIOD -> shift ? '>' : '.';
case GLFW.GLFW_KEY_COMMA -> shift ? '<' : ',';
case GLFW.GLFW_KEY_MINUS -> shift ? '_' : '-';
case GLFW.GLFW_KEY_EQUAL -> shift ? '+' : '=';
case GLFW.GLFW_KEY_SEMICOLON -> shift ? ':' : ';';
case GLFW.GLFW_KEY_APOSTROPHE -> shift ? '"' : '\'';
case GLFW.GLFW_KEY_SLASH -> shift ? '?' : '/';
case GLFW.GLFW_KEY_BACKSLASH -> shift ? '|' : '\\';
case GLFW.GLFW_KEY_LEFT_BRACKET -> shift ? '{' : '[';
case GLFW.GLFW_KEY_RIGHT_BRACKET -> shift ? '}' : ']';
case GLFW.GLFW_KEY_GRAVE_ACCENT -> shift ? '~' : '`';
default -> '\0';
};
}
public void saturnTick() {
synchronized (scheduler) {
if (f < 15.0f) {
f += 0.25f;
}
}
synchronized (widgets) {
for (SaturnWidget widget : widgets) {
if (widget.visible && widget.animations != null) {
for (SaturnAnimation animation : widget.animations) {
if (animation != null &&
tick - animation.lastTick == animation.delay) {
if (animation.run(widget, tick)) {
animation = null;
} else {
animation.lastTick = tick;
}
}
}
}
widget.tick();
}
tick++;
}
}
@Override
public void resize(MinecraftClient client, int width, int height) {
super.resize(client, width, height);
refresh();
}
}
@@ -1,88 +0,0 @@
package org.saturnclient.ui;
import net.minecraft.client.gui.DrawContext;
public class SaturnWidget {
public int x = 0;
public int y = 0;
public int width = 0;
public int height = 0;
public float alpha = 1.0f;
public float scale = 1.0f;
public SaturnAnimation[] animations = null;
public boolean visible = true;
public boolean focused = false;
public void init() {
}
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
}
public void click(int mouseX, int mouseY) {
}
public void keyPressed(int keyCode, int scanCode, int modifiers) {
}
public void charTyped(char typedChar) {
}
public void tick() {
}
public void mouseScrolled(
int mouseX,
int mouseY,
double horizontalAmount,
double verticalAmount) {
}
public SaturnWidget setX(int x) {
this.x = x;
return this;
}
public SaturnWidget setY(int y) {
this.y = y;
return this;
}
public SaturnWidget setWidth(int width) {
this.width = width;
return this;
}
public SaturnWidget setHeight(int height) {
this.height = height;
return this;
}
public SaturnWidget setAlpha(float alpha) {
this.alpha = alpha;
return this;
}
public SaturnWidget setAnimations(SaturnAnimation... animations) {
this.animations = animations;
for (SaturnAnimation anim : this.animations) {
anim.init(this);
}
return this;
}
public SaturnWidget setVisible(boolean visible) {
this.visible = visible;
return this;
}
public SaturnWidget setScale(float scale) {
this.scale = scale;
return this;
}
}
@@ -1,44 +0,0 @@
package org.saturnclient.ui;
import net.minecraft.util.Identifier;
public class Textures {
public static final Identifier COSMETICS = Identifier.of("saturnclient", "textures/gui/components/cosmetics.png");
public static final Identifier HUD_ICON = Identifier.of("saturnclient", "textures/gui/components/hud.png");
public static final Identifier SEARCH = Identifier.of("saturnclient", "textures/gui/components/search.png");
public static final Identifier CLOSE = Identifier.of("saturnclient", "textures/gui/components/close.png");
public static final Identifier MODS_TAB = Identifier.of("saturnclient", "textures/gui/components/mods_tab.png");
public static final Identifier SETTINGS = Identifier.of("saturnclient", "textures/gui/components/settings.png");
public static final Identifier CLOAK = Identifier.of("saturnclient", "textures/gui/components/cloak.png");
public static final Identifier HAT = Identifier.of("saturnclient", "textures/gui/components/hat.png");
public static final Identifier RESET = Identifier.of("saturnclient", "textures/gui/components/reset.png");
public static final Identifier TOGGLE_INDICATOR = Identifier.of("saturnclient",
"textures/gui/components/toggle_indicator.png");
public static final Identifier TOGGLE_BG = Identifier.of("saturnclient", "textures/gui/components/toggle_bg.png");
public static final Identifier LOGO = Identifier.of("saturnclient", "textures/logo/logo.png");
public static final Identifier REALISTIC_LOGO = Identifier.of("saturnclient", "textures/logo/realistic.png");
public static final Identifier LOGO_TEXT = Identifier.of("saturnclient", "textures/logo/text.png");
// Widgets
public static final Identifier BUTTON_BORDER = Identifier.of("saturnclient", "button_border");
public static final Identifier BUTTON = Identifier.of("saturnclient", "button");
public static final Identifier BUTTON_OP = Identifier.of("saturnclient", "button_op");
public static final Identifier SETTINGS_BG = Identifier.of("saturnclient", "settings_bg");
public static final Identifier RECT = Identifier.of("saturnclient", "rect");
public static final Identifier RECT_BORDER = Identifier.of("saturnclient", "rect_border");
public static final Identifier MOD = Identifier.of("saturnclient", "textures/gui/components/mod.png");
public static final Identifier MOD_BG = Identifier.of("saturnclient", "textures/gui/components/mod_bg.png");
public static Identifier getCloakPreview(String cloak) {
return Identifier.of("saturnclient", "textures/gui/cloak/" + (cloak.isEmpty() ? "none" : cloak) + ".png");
}
public static Identifier getHatPreview(String hat) {
return Identifier.of("saturnclient", "textures/gui/hat/" + (hat.isEmpty() ? "none" : hat) + ".png");
}
public static Identifier getModIcon(String modId) {
return Identifier.of("saturnclient", "textures/gui/mod/" + modId + ".png");
}
}
@@ -1,20 +0,0 @@
package org.saturnclient.ui.animations;
import org.saturnclient.ui.SaturnAnimation;
import org.saturnclient.ui.SaturnWidget;
public class FadeIn extends SaturnAnimation {
public FadeIn(int delay) {
this.delay = delay;
}
public void init(SaturnWidget widget) {
widget.alpha = 0.0f;
}
public boolean run(SaturnWidget widget, int tick) {
widget.alpha += 0.05f;
return widget.alpha > 0.9f;
}
}
@@ -1,24 +0,0 @@
package org.saturnclient.ui.animations;
import org.saturnclient.ui.SaturnAnimation;
import org.saturnclient.ui.SaturnWidget;
public class Slide extends SaturnAnimation {
private int offset;
public Slide(int delay, int offset) {
this.delay = delay;
this.offset = offset;
}
public void init(SaturnWidget widget) {
widget.y += offset;
}
public boolean run(SaturnWidget widget, int tick) {
widget.y--;
offset--;
return offset == 0;
}
}
@@ -1,179 +0,0 @@
package org.saturnclient.ui.components;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
public class SaturnFloat extends SaturnWidget {
String text = "";
public Property<Float> prop;
public int cursorPosition = 0;
private TextRenderer textRenderer = SaturnClient.client.textRenderer;
public SaturnFloat(Property<Float> prop, int x, int y, int width) {
this.x = x;
this.y = y;
this.width = width;
this.prop = prop;
this.text = String.valueOf(prop.value);
this.height = textRenderer.fontHeight + 4;
this.scale = 0.8f;
}
@Override
public void click(int mouseX, int mouseY) {
int scrollOffset = getScrollOffset();
String visibleText = text.substring(scrollOffset);
int newCursorPos = scrollOffset;
int textWidth = 0;
for (int i = 0; i < visibleText.length(); i++) {
textWidth = textRenderer.getWidth(visibleText.substring(0, i + 1));
if (textWidth > mouseX) {
break;
}
newCursorPos++;
}
cursorPosition = newCursorPos;
}
@Override
public void charTyped(char chr) {
if ((Character.isDigit(chr) || chr == '.') && text.length() < 10) {
if (text.equals("0") && cursorPosition == 1) {
text = "" + chr;
} else {
text = text.substring(0, cursorPosition) +
chr +
text.substring(cursorPosition);
prop.value = text.isEmpty() ? 0 : Float.parseFloat(text);
ConfigManager.save();
cursorPosition++;
}
}
}
@Override
public void keyPressed(int keyCode, int scanCode, int modifiers) {
if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
if (cursorPosition > 0) {
text = text.substring(0, cursorPosition - 1) +
text.substring(cursorPosition);
cursorPosition--;
if (text.length() == 0) {
text = "0";
cursorPosition = 1;
}
prop.value = Float.parseFloat(text);
ConfigManager.save();
}
} else if (keyCode == GLFW.GLFW_KEY_LEFT) {
if (cursorPosition > 0) {
cursorPosition--;
}
} else if (keyCode == GLFW.GLFW_KEY_RIGHT) {
if (cursorPosition < text.length()) {
cursorPosition++;
}
}
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
if (prop.isReset) {
this.text = String.valueOf(prop.value);
this.cursorPosition = 0;
prop.isReset = false;
}
SaturnUi.drawHighResGuiTexture(
context,
Textures.BUTTON_BORDER,
0,
0,
this.width,
this.height,
focused
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(this.alpha));
int scrollOffset = getScrollOffset();
String visibleText = getVisibleText(scrollOffset);
int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
context.drawText(
textRenderer,
SaturnUi.text(visibleText),
2,
2,
textColor,
false);
if (focused) {
int cursorX = 2 +
textRenderer.getWidth(
SaturnUi.text(
visibleText.substring(0, cursorPosition - scrollOffset)));
context.fill(
cursorX,
2,
cursorX + 1,
textRenderer.fontHeight + 4,
0xFFFFFFFF);
}
}
public void clearText() {
text = "0";
cursorPosition = 1;
prop.value = 0.0f;
}
private int getScrollOffset() {
if (text.isEmpty()) {
return 0;
}
int offset = 0;
int maxWidth = width - 4; // Available width for text
while (offset < cursorPosition) { // Ensure cursor remains visible
String visibleText = text.substring(offset, cursorPosition);
if (textRenderer.getWidth(SaturnUi.text(visibleText)) > maxWidth) {
offset++;
} else {
break;
}
}
return offset;
}
private String getVisibleText(int scrollOffset) {
String visibleText = text.substring(scrollOffset);
// Ensure text does not overflow past the available width
for (int i = 1; i <= visibleText.length(); i++) {
if (textRenderer.getWidth(
SaturnUi.text(visibleText.substring(0, i))) > width - 4) {
return visibleText.substring(0, i - 1); // Trim overflow
}
}
return visibleText;
}
}
@@ -1,140 +0,0 @@
package org.saturnclient.ui.components;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
public class SaturnHex extends SaturnWidget {
private String text = "#"; // Always starts with '#'
public Property<Integer> prop;
public int cursorPosition = 1; // After '#'
private final TextRenderer textRenderer = SaturnClient.client.textRenderer;
public SaturnHex(Property<Integer> prop, int x, int y, int width) {
this.x = x;
this.y = y;
this.width = width;
this.prop = prop;
this.text = intToHex(prop.value);
this.height = textRenderer.fontHeight + 4;
this.scale = 0.8f;
}
@Override
public void click(int mouseX, int mouseY) {
cursorPosition = Math.min(7, text.length()); // Keep cursor inside hex length
}
@Override
public void charTyped(char chr) {
if (isHexChar(chr) && text.length() < 7) { // Limit to 6 hex digits + '#'
text = text.substring(0, cursorPosition) +
chr +
text.substring(cursorPosition);
cursorPosition++;
if (text.length() == 7) { // Only update value when full hex is entered
prop.value = hexToInt(text);
ConfigManager.save();
}
}
}
@Override
public void keyPressed(int keyCode, int scanCode, int modifiers) {
if (keyCode == GLFW.GLFW_KEY_BACKSPACE && cursorPosition > 1) { // Prevent deleting '#'
text = text.substring(0, cursorPosition - 1) +
text.substring(cursorPosition);
cursorPosition--;
} else if (keyCode == GLFW.GLFW_KEY_LEFT && cursorPosition > 1) {
cursorPosition--;
} else if (keyCode == GLFW.GLFW_KEY_RIGHT && cursorPosition < text.length()) {
cursorPosition++;
}
if (text.length() == 1) { // Reset if empty (keep '#')
text = "#";
cursorPosition = 1;
}
if (text.length() == 7) { // Update only when valid hex
prop.value = hexToInt(text);
ConfigManager.save();
}
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
if (prop.isReset) {
this.text = intToHex(prop.value);
this.cursorPosition = 0;
prop.isReset = false;
}
SaturnUi.drawHighResGuiTexture(
context,
Textures.BUTTON_BORDER,
0,
0,
this.width,
this.height,
focused
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(this.alpha));
int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
context.drawText(
textRenderer,
SaturnUi.text(text),
2,
2,
textColor,
false);
if (focused) {
int cursorX = 2 +
textRenderer.getWidth(
SaturnUi.text(text.substring(0, cursorPosition)));
context.fill(
cursorX,
2,
cursorX + 1,
textRenderer.fontHeight + 4,
0xFFFFFFFF);
}
}
public void clearText() {
text = "#";
cursorPosition = 1;
prop.value = 0xFFFFFFFF; // Default white
}
private boolean isHexChar(char chr) {
return ((chr >= '0' && chr <= '9') ||
(chr >= 'A' && chr <= 'F') ||
(chr >= 'a' && chr <= 'f'));
}
public static int hexToInt(String hex) {
hex = hex.replace("#", "");
int rgb = Integer.parseInt(hex, 16);
return (0xFF << 24) | rgb;
}
public static String intToHex(int color) {
int rgb = color & 0xFFFFFF;
return String.format("#%06X", rgb).toLowerCase();
}
}
@@ -1,177 +0,0 @@
package org.saturnclient.ui.components;
import net.minecraft.client.gui.DrawContext;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
public class SaturnInteger extends SaturnWidget {
String text = "";
public Property<Integer> prop;
public int cursorPosition = 0;
public SaturnInteger(Property<Integer> prop, int x, int y, int width) {
this.x = x;
this.y = y;
this.width = width;
this.prop = prop;
this.text = String.valueOf(prop.value);
this.height = SaturnClient.client.textRenderer.fontHeight + 4;
this.scale = 0.8f;
}
@Override
public void click(int mouseX, int mouseY) {
int scrollOffset = getScrollOffset();
String visibleText = text.substring(scrollOffset);
int newCursorPos = scrollOffset;
int textWidth = 0;
for (int i = 0; i < visibleText.length(); i++) {
textWidth = SaturnClient.client.textRenderer.getWidth(visibleText.substring(0, i + 1));
if (textWidth > mouseX) {
break;
}
newCursorPos++;
}
cursorPosition = newCursorPos;
}
@Override
public void charTyped(char chr) {
if (Character.isDigit(chr) && text.length() < 10) {
if (text.equals("0") && cursorPosition == 1) {
text = "" + chr;
} else {
text = text.substring(0, cursorPosition) +
chr +
text.substring(cursorPosition);
prop.value = text.isEmpty() ? 0 : Integer.parseInt(text);
ConfigManager.save();
cursorPosition++;
}
}
}
@Override
public void keyPressed(int keyCode, int scanCode, int modifiers) {
if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
if (cursorPosition > 0) {
text = text.substring(0, cursorPosition - 1) +
text.substring(cursorPosition);
cursorPosition--;
if (text.length() == 0) {
text = "0";
cursorPosition = 1;
}
prop.value = Integer.parseInt(text);
ConfigManager.save();
}
} else if (keyCode == GLFW.GLFW_KEY_LEFT) {
if (cursorPosition > 0) {
cursorPosition--;
}
} else if (keyCode == GLFW.GLFW_KEY_RIGHT) {
if (cursorPosition < text.length()) {
cursorPosition++;
}
}
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
if (prop.isReset) {
this.text = String.valueOf(prop.value);
this.cursorPosition = 0;
prop.isReset = false;
}
SaturnUi.drawHighResGuiTexture(
context,
Textures.BUTTON_BORDER,
0,
0,
this.width,
this.height,
focused
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(this.alpha));
int scrollOffset = getScrollOffset();
String visibleText = getVisibleText(scrollOffset);
int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
context.drawText(
SaturnClient.client.textRenderer,
SaturnUi.text(visibleText),
2,
2,
textColor,
false);
if (focused) {
int cursorX = 2 +
SaturnClient.client.textRenderer.getWidth(
SaturnUi.text(
visibleText.substring(0, cursorPosition - scrollOffset)));
context.fill(
cursorX,
2,
cursorX + 1,
SaturnClient.client.textRenderer.fontHeight + 4,
0xFFFFFFFF);
}
}
public void clearText() {
text = "0";
cursorPosition = 1;
prop.value = 0;
}
private int getScrollOffset() {
if (text.isEmpty()) {
return 0;
}
int offset = 0;
int maxWidth = width - 4; // Available width for text
while (offset < cursorPosition) { // Ensure cursor remains visible
String visibleText = text.substring(offset, cursorPosition);
if (SaturnClient.client.textRenderer.getWidth(SaturnUi.text(visibleText)) > maxWidth) {
offset++;
} else {
break;
}
}
return offset;
}
private String getVisibleText(int scrollOffset) {
String visibleText = text.substring(scrollOffset);
// Ensure text does not overflow past the available width
for (int i = 1; i <= visibleText.length(); i++) {
if (SaturnClient.client.textRenderer.getWidth(
SaturnUi.text(visibleText.substring(0, i))) > width - 4) {
return visibleText.substring(0, i - 1); // Trim overflow
}
}
return visibleText;
}
}
@@ -1,106 +0,0 @@
package org.saturnclient.ui.components;
import net.minecraft.client.gui.DrawContext;
import org.saturnclient.modules.Module;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.menus.ConfigEditor;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
public class SaturnModComp extends SaturnWidget {
Module mod;
public SaturnModComp(Module mod) {
this.mod = mod;
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
boolean enabled = mod.isEnabled();
boolean hovering_settings = settingsHovering(mouseX, mouseY);
int color = !hovering_settings && (enabled || hovering)
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(alpha);
int size = 12;
if (enabled) {
SaturnUi.drawHighResTexture(
context,
Textures.MOD_BG,
0,
0,
86,
20,
SaturnClientConfig.color.value);
}
SaturnUi.drawHighResTexture(
context,
Textures.MOD,
0,
0,
86,
20,
color);
SaturnUi.drawHighResTexture(
context,
mod.getIconTexture(),
4,
4,
size,
size,
color);
context.drawText(
SaturnClient.client.textRenderer,
SaturnUi.text(mod.getName()),
20,
(21 - SaturnClient.client.textRenderer.fontHeight) / 2,
color,
false);
SaturnUi.drawHighResTexture(
context,
Textures.SETTINGS,
77,
11,
7,
7,
hovering_settings
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(alpha));
}
@Override
public void click(int mouseX, int mouseY) {
if (settingsHovering(mouseX, mouseY)) {
SaturnClient.client
.setScreen(new ConfigEditor(mod.getConfig()));
} else {
mod.setEnabled(!mod.isEnabled());
}
}
public boolean settingsHovering(int mouseX, int mouseY) {
int sX = 77;
int sY = 11;
int size = 7;
return (sX < mouseX &&
sX + size > mouseX &&
sY < mouseY &&
sY + size > mouseY);
}
}
@@ -1,155 +0,0 @@
package org.saturnclient.ui.components;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
public class SaturnString extends SaturnWidget {
public Property<String> prop;
public int cursorPosition = 0;
private TextRenderer textRenderer = SaturnClient.client.textRenderer;
public SaturnString(Property<String> prop, int x, int y, int width) {
this.x = x;
this.y = y;
this.width = width;
this.prop = prop;
this.height = textRenderer.fontHeight + 4;
this.scale = 0.8f;
}
@Override
public void click(int mouseX, int mouseY) {
int scrollOffset = getScrollOffset();
String visibleText = prop.value.substring(scrollOffset);
int newCursorPos = scrollOffset;
int textWidth = 0;
for (int i = 0; i < visibleText.length(); i++) {
textWidth = textRenderer.getWidth(visibleText.substring(0, i + 1));
if (textWidth > mouseX) {
break;
}
newCursorPos++;
}
cursorPosition = newCursorPos;
}
@Override
public void charTyped(char chr) {
prop.value = prop.value.substring(0, cursorPosition) +
chr +
prop.value.substring(cursorPosition);
cursorPosition++;
}
@Override
public void keyPressed(int keyCode, int scanCode, int modifiers) {
if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
if (cursorPosition > 0) {
prop.value = prop.value.substring(0, cursorPosition - 1) +
prop.value.substring(cursorPosition);
cursorPosition--;
}
} else if (keyCode == GLFW.GLFW_KEY_LEFT) {
if (cursorPosition > 0) {
cursorPosition--;
}
} else if (keyCode == GLFW.GLFW_KEY_RIGHT) {
if (cursorPosition < prop.value.length()) {
cursorPosition++;
}
}
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
SaturnUi.drawHighResGuiTexture(
context,
Textures.BUTTON_BORDER,
0,
0,
this.width,
this.height,
focused
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(this.alpha));
int scrollOffset = getScrollOffset();
String visibleText = getVisibleText(scrollOffset);
int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
context.drawText(
textRenderer,
SaturnUi.text(visibleText),
2,
2,
textColor,
false);
if (focused) {
int cursorX = 2 +
textRenderer.getWidth(
SaturnUi.text(
visibleText.substring(0, cursorPosition - scrollOffset)));
context.fill(
cursorX,
2,
cursorX + 1,
textRenderer.fontHeight + 3,
0xFFFFFFFF);
}
}
public void clearText() {
prop.value = "";
cursorPosition = 1;
}
private int getScrollOffset() {
if (prop.value.isEmpty()) {
return 0;
}
int offset = 0;
int maxWidth = width - 4; // Available width for prop.value
while (offset < cursorPosition) { // Ensure cursor remains visible
String visibleText = prop.value.substring(offset, cursorPosition);
if (textRenderer.getWidth(SaturnUi.text(visibleText)) > maxWidth) {
offset++;
} else {
break;
}
}
return offset;
}
private String getVisibleText(int scrollOffset) {
String visibleText = prop.value.substring(scrollOffset);
// Ensure prop.value does not overflow past the available width
for (int i = 1; i <= visibleText.length(); i++) {
if (textRenderer.getWidth(
SaturnUi.text(visibleText.substring(0, i))) > width - 4) {
return visibleText.substring(0, i - 1); // Trim overflow
}
}
return visibleText;
}
}
@@ -1,66 +0,0 @@
package org.saturnclient.ui.components;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.render.RenderLayer;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
public class SaturnToggle extends SaturnWidget {
Property<Boolean> prop;
public SaturnToggle(Property<Boolean> prop) {
this.prop = prop;
width = 16;
height = 8;
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
int color = prop.value
? SaturnClientConfig.color.value
: SaturnClientConfig.getWhite(alpha);
int w = 13;
int h = 6;
int s = 8;
context.drawTexture(
RenderLayer::getGuiTextured,
Textures.TOGGLE_BG,
0,
1,
0,
0,
w,
h,
w,
h,
color);
context.drawTexture(
RenderLayer::getGuiTextured,
Textures.TOGGLE_INDICATOR,
prop.value ? 5 : 0,
0,
0,
0,
s,
s,
s,
s,
color);
}
@Override
public void click(int mouseX, int mouseY) {
prop.value = !prop.value;
ConfigManager.save();
}
}
@@ -1,106 +0,0 @@
package org.saturnclient.ui.widgets;
import com.mojang.blaze3d.systems.RenderSystem;
import java.util.function.Consumer;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.text.Text;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
import org.saturnclient.ui.Textures;
import net.minecraft.client.MinecraftClient;
public class SaturnButton extends SaturnWidget {
public boolean active = true;
public String text;
public Consumer<SaturnButton> onPress;
public boolean background = true;
public boolean bold = false;
private float hoverAlpha = 0.0f;
public SaturnButton(String text, Consumer<SaturnButton> onPress) {
this.text = text;
this.onPress = onPress;
}
public SaturnButton(String text, Runnable onPress) {
this(text, b -> {
onPress.run();
});
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
MinecraftClient minecraftClient = SaturnClient.client;
if (background) {
RenderSystem.setShaderTexture(0, Textures.BUTTON);
SaturnUi.drawHighResGuiTexture(
context,
Textures.BUTTON,
0,
0,
this.width,
this.height,
SaturnClientConfig.getColor(hovering, alpha));
if (hovering) {
if (hoverAlpha == 0.0f)
hoverAlpha = 0.1f;
SaturnUi.drawHighResGuiTexture(
context,
Textures.BUTTON_BORDER,
0,
0,
this.width,
this.height,
SaturnClientConfig.getColor(hovering, alpha));
} else {
hoverAlpha = 0.0f;
}
}
if (alpha != 0.0f) {
Text t = bold ? SaturnUi.textBold(text) : SaturnUi.text(text);
context.drawText(
minecraftClient.textRenderer,
t,
((this.width -
minecraftClient.textRenderer.getWidth(t))
/
2),
((this.height - minecraftClient.textRenderer.fontHeight + 1) /
2),
SaturnClientConfig.getColor(hovering, alpha),
false);
}
}
@Override
public void click(int mouseX, int mouseY) {
this.onPress.accept(this);
}
@Override
public void tick() {
if (hoverAlpha > 0.0f && hoverAlpha < 0.9f)
hoverAlpha += 0.03f;
else if (hoverAlpha > 0.9f && hoverAlpha < 1.0f)
hoverAlpha = 1.0f;
}
public SaturnWidget setBold(boolean bold) {
this.bold = bold;
return this;
}
}
@@ -1,68 +0,0 @@
package org.saturnclient.ui.widgets;
import java.util.function.Consumer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.util.Identifier;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
public class SaturnClickableImage extends SaturnWidget {
public Identifier texture;
public boolean selected = false;
public Consumer<SaturnClickableImage> onPress;
private float hoverAlpha = 0.0f;
public SaturnClickableImage(
Identifier texture,
Consumer<SaturnClickableImage> onPress) {
this.texture = texture;
this.onPress = onPress;
}
public SaturnClickableImage(Identifier texture, Runnable onPress) {
this(texture, _o -> {
onPress.run();
});
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
if (hovering) {
if (hoverAlpha == 0.0f)
hoverAlpha = 0.1f;
}
SaturnUi.drawHighResTexture(
context,
texture,
0,
0,
width,
height,
SaturnClientConfig.getColor(hovering || selected, alpha));
}
@Override
public void click(int mouseX, int mouseY) {
this.onPress.accept(this);
}
public SaturnClickableImage setSelected(boolean selected) {
this.selected = selected;
return this;
}
@Override
public void tick() {
if (hoverAlpha > 0.0f && hoverAlpha < 0.9f)
hoverAlpha += 0.03f;
else if (hoverAlpha > 0.9f && hoverAlpha < 1.0f)
hoverAlpha = 1.0f;
}
}
@@ -1,54 +0,0 @@
package org.saturnclient.ui.widgets;
import java.util.function.Consumer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.util.Identifier;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.ui.SaturnUi;
import org.saturnclient.ui.SaturnWidget;
public class SaturnClickableSprite extends SaturnWidget {
public Identifier sprite;
public boolean selected = false;
public Consumer<SaturnClickableSprite> onPress;
public SaturnClickableSprite(
Identifier sprite,
Consumer<SaturnClickableSprite> onPress) {
this.sprite = sprite;
this.onPress = onPress;
}
public SaturnClickableSprite(Identifier sprite, Runnable onPress) {
this(sprite, _o -> {
onPress.run();
});
}
@Override
public void render(
DrawContext context,
boolean hovering,
int mouseX,
int mouseY) {
SaturnUi.drawHighResGuiTexture(
context,
sprite,
0,
0,
this.width,
this.height,
SaturnClientConfig.getColor(hovering, alpha));
}
@Override
public void click(int mouseX, int mouseY) {
this.onPress.accept(this);
}
public SaturnClickableSprite setSelected(boolean selected) {
this.selected = selected;
return this;
}
}

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