Compare commits

...
212 Commits
Author SHA1 Message Date
Jones af12ef535c Release 2.1.26 (#453) 2025-02-28 16:16:22 +01:00
Jones 7de08ffa98 Fix wrong condition in BungeeCord injector 2025-02-28 16:13:28 +01:00
dependabot[bot] c56d41cd77 Bump io.netty:netty-all from 4.1.117.Final to 4.1.118.Final (#451)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.117.Final to 4.1.118.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 07:20:44 +01:00
Jones 4b30729138 Release 2.1.25 (#449) 2025-02-16 15:45:44 +01:00
Jones 1254514ad2 Improve BungeeCord injection method 2025-02-15 21:23:15 +01:00
Jones b4673d8e9f Prioritize BungeeCord's channel acceptor 2025-02-15 20:47:16 +01:00
Jones 5eaaf88ec8 Tell BungeeCord users about Velocity 2025-02-15 20:15:22 +01:00
Jones 367ce07cb0 Fix incompatibility with latest BungeeCord 2025-02-15 20:13:14 +01:00
Jones 901b49b063 Release 2.1.24 (#447) 2025-02-13 13:58:38 +01:00
Jones a3cdc08f90 Use a real level name for the JoinGame packet 2025-02-07 11:56:54 +01:00
Jones 150a3575b2 Replace SuppressWarnings with noinspection 2025-02-07 11:56:25 +01:00
Jones 6082bf944d Implement safe Sonar API get() method 2025-02-07 11:55:03 +01:00
Jones e6d20e2b94 Add some basic doc comments to Sonar API 2025-02-07 11:52:03 +01:00
Jones 6f3a8432a7 Merge remote-tracking branch 'origin/main' 2025-02-07 11:49:49 +01:00
Jones 350ae02ff0 Mark ambiguous dimensions as deprecated 2025-02-07 11:47:26 +01:00
dependabot[bot] 32c37c0337 Bump com.gradleup.shadow from 8.3.5 to 8.3.6 (#445)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.5 to 8.3.6.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.5...8.3.6)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 01:15:30 +01:00
Jones b8e552dff9 Bump Gradle from 8.12 to 8.12.1 2025-02-01 22:16:36 +01:00
Jones 39902c2c9b Release 2.1.23 (#442) 2025-01-29 16:24:29 +01:00
Jones 3324ee6238 Link documentation in transfer mode section 2025-01-27 15:57:04 +01:00
dependabot[bot] 732a291417 Bump org.jetbrains:annotations from 26.0.1 to 26.0.2 (#441)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 26.0.1 to 26.0.2.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/26.0.1...26.0.2)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 02:21:12 +01:00
Jones 778ac20e73 Link documentation in language.properties 2025-01-26 16:12:20 +01:00
Renan GabrielandJones 8cc49884ec Add Brazilian Portuguese (pt-BR) translations (#440)
* Add Brazilian Portuguese (pt-BR) translations

- Created translation files for Brazilian Portuguese:
  - `webhook/pt-BR.yml`
  - `messages/pt-BR.yml`
  - `config/pt-BR.yml`

This contribution adds support for Brazilian Portuguese, enhancing accessibility for Portuguese-speaking users.

* Add extended support for pt-br (ISO)

* Ignore casing in language codes

---------

Co-authored-by: Jones <admin@jonesdev.xyz>
2025-01-26 16:10:24 +01:00
Jones 798ba4bfd1 Fix race condition in VerifiedPlayerController 2025-01-25 17:52:23 +01:00
Jones 5139a63ff7 Bump caffeine from 3.1.8 to 3.2.0 2025-01-20 14:59:36 +01:00
dependabot[bot] 06d81f1ec0 Bump com.github.ben-manes.caffeine:caffeine from 3.1.8 to 3.2.0 (#436)
Bumps [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) from 3.1.8 to 3.2.0.
- [Release notes](https://github.com/ben-manes/caffeine/releases)
- [Commits](https://github.com/ben-manes/caffeine/compare/v3.1.8...v3.2.0)

---
updated-dependencies:
- dependency-name: com.github.ben-manes.caffeine:caffeine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 14:29:08 +01:00
dependabot[bot] d08de99e03 Bump com.diffplug.spotless from 7.0.1 to 7.0.2 (#435)
Bumps com.diffplug.spotless from 7.0.1 to 7.0.2.

---
updated-dependencies:
- dependency-name: com.diffplug.spotless
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 14:22:34 +01:00
dependabot[bot] 411cadb5c3 Bump io.netty:netty-all from 4.1.116.Final to 4.1.117.Final (#437)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.116.Final to 4.1.117.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.116.Final...netty-4.1.117.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 14:22:11 +01:00
Jones ab99061b93 Release 2.1.22 (#434) 2025-01-18 11:16:27 +01:00
Jones 7d7f1d97d9 Fix GitHub workflow build error 2025-01-16 17:08:15 +01:00
JonesandAkkariin 9f4cc49cf7 Use full registries to fix compatibility (#429)
* Add full registries to fix compatibility issues

* Fix 1.18.2-1.19.1 registries

* Fix codec nbt loading error

* Optimize 1.20-1.21.4 codecs

* Registries fixes & Fixed some unexpected behavior about mods. (#430)

* Fix 1.20.5 + viafabricplus

* Update registries

* Apply SIMPLE_BIOME filter on 1.18.2-1.21.4 codecs

* Use 1.21-1.21.4 codecs from the previous commit

---------

Co-authored-by: Akkariin <fallencrystal@outlook.com>
2025-01-16 16:50:31 +01:00
Jones 8531dcc58a Downgrade mariadb-java-client to 3.4.1 2025-01-15 19:34:57 +01:00
dependabot[bot] ec498000ba Bump com.diffplug.spotless from 7.0.0.BETA4 to 7.0.1 (#432)
Bumps com.diffplug.spotless from 7.0.0.BETA4 to 7.0.1.

---
updated-dependencies:
- dependency-name: com.diffplug.spotless
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 14:44:52 +01:00
Jones 9ac46ce5a4 Bump Gradle from 8.11.1 to 8.12 2025-01-11 14:25:00 +01:00
Jones 7848e47990 Fix invalid block state ids for 1.21.4 2025-01-04 05:51:11 +01:00
Jones e377644e89 Update license year 2025-01-03 05:09:58 +01:00
Jones 9f377780cb Release 2.1.21 (#427) 2025-01-01 20:21:59 +01:00
Jones f14c5b2b90 Fix NoClassDefFoundError on BungeeCord and Bukkit 2025-01-01 20:20:13 +01:00
Jones 45683040ab Release 2.1.20 (#426) 2024-12-31 17:49:13 +01:00
Jones d0f63bea48 Update 1.19-1.21.2 codecs
* Backport codecs from Sonar 3.0
2024-12-31 17:46:48 +01:00
Jones 030a532df8 Remove some misleading TODOs 2024-12-30 05:27:24 +01:00
Jones 0f957b444b Bump dynamic adventure libraries 2024-12-23 13:42:48 +01:00
dependabot[bot] cb03833a8a Bump adventure from 4.17.0 to 4.18.0 (#422)
Bumps `adventure` from 4.17.0 to 4.18.0.

Updates `net.kyori:adventure-text-minimessage` from 4.17.0 to 4.18.0
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.17.0...v4.18.0)

Updates `net.kyori:adventure-text-serializer-gson` from 4.17.0 to 4.18.0
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.17.0...v4.18.0)

Updates `net.kyori:adventure-nbt` from 4.17.0 to 4.18.0
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.17.0...v4.18.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-text-minimessage
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: net.kyori:adventure-text-serializer-gson
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: net.kyori:adventure-nbt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 13:42:16 +01:00
dependabot[bot] 22f2ab56bc Bump io.netty:netty-all from 4.1.115.Final to 4.1.116.Final (#423)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.115.Final to 4.1.116.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.115.Final...netty-4.1.116.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 05:18:32 +01:00
Jones cda6b395ac Make ComponentHolder#serialize static 2024-12-20 16:50:31 +01:00
Jones 46c5853f7c Remove mentions of verification timing "NEVER" 2024-12-20 16:35:54 +01:00
Jones cf4713b2d8 Disable lombok generated annotations 2024-12-20 16:24:21 +01:00
Jones f82f0ddd87 Remove spotless license header check 2024-12-14 13:32:00 +01:00
Jones 7f3d244030 Release 2.1.19 (#420) 2024-12-06 16:58:52 +01:00
JonesandAkkariin 3db8230e9f Minecraft 1.21.4 Support (#408)
* Add missing server protocol versions

* Add 1.21.4 client protocol version

* Fix SetHeldItemPacket for 1.21.4

* Fix codestyle issue caused by the merge

* Fix CI build error

* 1.21.4-pre1 support (#415)

* Update 1.21.4 protocol version to pre-release 1

* Add 1.21.4 registry

* Optimize the size of the `codec_1_21_2.nbt` registry.

* Update entity/item/block id mapping for 1.21.4-pre1

* Update packet registry for 1.21.4-pre1

* Add `onGround` field for `VehicleMovePacket` decode on 1.21.4-pre1.

* Minecraft 1.21.4-pre3 Support

* Minecraft 1.21.4-rc3 Support

* Fix VehicleMovePacket being broken for 1.21.3-

* Update codec for 1.21.4(-rc3)

* Fix 1.21.4 codec and protocol version

---------

Co-authored-by: Akkariin <fallencrystal@outlook.com>
2024-12-04 20:03:36 +01:00
Jones 8295119016 Remove unused TeleportEntityPacket 2024-12-01 15:28:06 +01:00
Jones a0b14214ae Release 2.1.18 (#418) 2024-12-01 14:00:32 +01:00
Jones 3d96a32f81 Fix crash issue on 1.13-1.13.2
(Backported minor changes from 3.0.0)
2024-12-01 01:23:54 +01:00
Jones de67ab12c2 Downgrade mysql-connector-j to 9.0.0 2024-11-30 19:46:08 +01:00
Jones adef9d9ccb Release 2.1.17 (#417) 2024-11-26 17:06:20 +01:00
Jones 71b40af656 Bump mariadb-java-client from 3.4.1 to 3.5.1 2024-11-23 17:07:21 +01:00
Jones 3a79711a8b Bump mysql-connector-j from 9.0.0 to 9.1.0 2024-11-23 17:06:29 +01:00
Jones 3094f12f84 Bump Gradle from 8.11 to 8.11.1 2024-11-23 17:02:51 +01:00
Jones a74c3da8d4 Use version name instead of ID for logging 2024-11-23 15:13:48 +01:00
dependabot[bot] 427768fc43 Bump io.netty:netty-all from 4.1.114.Final to 4.1.115.Final (#412)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.114.Final to 4.1.115.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.114.Final...netty-4.1.115.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 03:57:07 +01:00
dependabot[bot] b0d7dfb735 Bump org.projectlombok:lombok from 1.18.34 to 1.18.36 (#413)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.34 to 1.18.36.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.34...v1.18.36)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 03:56:53 +01:00
Jones ff9216c4cc Add spotless for codestyle (#410)
* Add spotless for codestyle

* Fix license header file

* Bump Gradle from 8.10.2 to 8.11
2024-11-11 17:52:52 +01:00
Jones 43a1aa2ae8 Release 2.1.16 (#409) 2024-11-11 14:14:04 +01:00
Jones f36a613124 Clear ByteBuf if the packet has no start 2024-11-08 15:53:39 +01:00
Jones 3e193bddd7 Fix "invalid username" never being used 2024-11-07 18:44:43 +01:00
Jones 66d0651d1e Release 2.1.15 (#407) 2024-11-04 15:35:41 +01:00
dependabot[bot] b21c4e8a9a Bump com.gradleup.shadow from 8.3.3 to 8.3.5 (#406)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.3 to 8.3.5.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.3...8.3.5)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 15:23:40 +01:00
Jones b77e0c4806 Remove accidental debugging statement 2024-11-03 12:36:04 +01:00
Jones cb76dfe06e Release 2.1.14 (#405) 2024-11-02 14:59:53 +01:00
Jones d0a758473f Move username check to post-login 2024-11-02 14:57:11 +01:00
Jones e4dfab87da Fix pre-join disconnects not encoding correctly 2024-11-02 14:53:57 +01:00
Jones 2fe3ab1d8d Make unsupported version message configurable 2024-11-02 14:53:27 +01:00
Akkariin 4483e52852 Update language/config files for chinese. (#404)
* Update language/config files for chinese.

* Emphasizing parts makes it an accurate description of something.

* [ci skip] Fix space
2024-10-31 22:01:24 +01:00
Jones 22b41151d6 [ci skip] Display sponsors in README 2024-10-30 18:12:58 +01:00
Jones e849060db1 [ci skip] Add FUNDING.yml 2024-10-28 17:11:23 +01:00
Jones 59b338e775 Add client ticking check for 1.21.2+ 2024-10-27 15:50:44 +01:00
Jones e33ee1333a Release 2.1.13 (#403) 2024-10-26 17:10:37 +02:00
Jones 50e6b017f8 Don't serialize modern json twice 2024-10-26 17:08:46 +02:00
Jones f25cc18cb9 Fix NoClassDefFoundError on BungeeCord and Bukkit 2024-10-26 17:08:29 +02:00
Jones a7b1bfd958 Release 2.1.12 (#402) 2024-10-26 16:01:30 +02:00
Jones d97f124201 Fix timestamp check false positive 2024-10-26 15:30:34 +02:00
Jones 5e9d8b4b29 Optimize packet size checking 2024-10-26 15:07:54 +02:00
Jones c3eabd1265 Fix component serialization on <1.16 (https://github.com/jonesdevelopment/sonar/issues/401) 2024-10-24 14:19:21 +02:00
Jones 9d2752dc32 Release 2.1.11 (#400) 2024-10-23 15:27:19 +02:00
Jones 5fe9c18268 Move "FallbackTimeoutHandler" to "netty" 2024-10-23 02:33:06 +02:00
Jones f93dae9532 Bump Gradle from 8.10.1 to 8.10.2 2024-10-22 21:08:45 +02:00
Jones ab36823ca2 Microoptimize cached tab suggestions 2024-10-22 21:04:56 +02:00
Jones 5c94f06510 Remove useless variables from some events 2024-10-22 20:55:01 +02:00
Jones d069b9aadb Fixed CAPTCHA Y position breaking lighting 2024-10-22 20:44:37 +02:00
Jones 3337009fe4 Fix filled_map item type ID 2024-10-22 20:44:14 +02:00
Jones b010e18552 Remove debug message from encoder 2024-10-22 20:37:47 +02:00
Jones 82b4b3bac7 Optimize EntityType and ItemType 2024-10-22 20:26:59 +02:00
Jones b143ea548a Refactor some protocol-related classes 2024-10-22 20:24:39 +02:00
Jones 689e3ea36b Fix UpdateTime packet on 1.21.2 2024-10-22 20:16:25 +02:00
Jones c9c1a2abb7 Microoptimize "/sonar statistics" 2024-10-22 19:46:44 +02:00
Jones a8aa284a5e Simplify CaffeineCacheRatelimiter initialization 2024-10-22 19:07:22 +02:00
448f8d9af3 1.21.2 Support (#370)
* feat: register 1.21.2 protocol ID

* Fix LoginSuccessPacket for 1.21.2

* Backport changes from https://github.com/jonesdevelopment/sonar/pull/370/files#diff-4e3d725f29d513f2f57f369660b93a7ee1d6414004158562736e2e2976fdf121

* Use Velocity's `writeBinaryTag` method

* Rename some variables for clarity

* Use Velocity's version comparison methods

* Add missing signature checks for 1.19-1.19.2

* Simplify uuid reading in LoginStartPacket

* Add missing chat timestamp checks

* Recode JoinGame packet (and dimensions)

* Add particleStatus to ClientInformationPacket

* Fix teleport incompatibilities on 1.16-1.16.5

* Fix invalid Y position handling

* Remove clientbound array length check

* Improve readability of ChunkDataPacket

* Set default write-timeout to 15000

* Set default rejoin-delay to 5000

* [wip] FallbackVehicleHandler 1.21.2 Support

* Recode the entire vehicle handler

* Fix lag compensation in the vehicle check

* Fix previous merge conflicts (origin/main)

* Inline static strings for better readability

* Remove debugging in vehicle handler

* Set default write-timeout to 10000

* Reduce the number of static imports

* Strictly exempt Geyser from PM/CS checks

* Micro-optimize Geyser check

* Remove unnecessary variable assignment

* Fix some false positives in the vehicle check

* 1.21.2-pre4 Support (#397)

* packet id mapping. tags. pre-3 protocol version. empty codec.

* Fix build. Added `codec_1_21_2.nbt` generated by `NBTGenerator`

* Fix `SetPlayerPositionRotationPacket` encode for 1.21.2-pre3

* Fix wrong `encodeOnly` in client bound KeepAlive registry

* Small codestyle improvements

* Fix `ChunkDataPacket` encode for 1.21.2-pre3

* Fix missing packet registry.

* Fix entity type id for 1.21.2-pre3

* Fix wrong map component id for 1.21.2-pre3

* Add 1.21.2-pre3 block mapping.

* Remove update tags packet. (Only affect crafting recipes)

* Remove tag registry.

* Fix and improve vehicle teleport logic for 1.21.2-pre3

* Set `exceptTeleport` to false after received `TeleportConfirm`

* Fix typo for `expectTeleport`

* Minor codestyle changes

* Fix unexpected behavior in vehicle checks.

* Remove debug log

* Simply fix gravity checks for 1.21.2-pre3

* Change protocol version to pre4

* Fix wrong `encodeOnly` state in packet registry

* Fix some previously introduced merging issues

* Update to 1.21.2-pre5

* Fix possible bug in protocol handler

* Fix wrong `encodeOnly` state

---------

Co-authored-by: Jones <admin@jonesdev.xyz>

* Fix KeepAlive check not working on 1.20.2+

* Don't cap KeepAlive ID in pre-join

* Small codestyle improvements

* Fix a false positive in the vehicle check

* Remove tailing whitespace

* Minecraft 1.21.2-rc1 support

* Minecraft 1.21.2 support

---------

Co-authored-by: Akkariin <fallencrystal@outlook.com>
2024-10-22 18:58:23 +02:00
dependabot[bot] 3d8f61fce3 Bump org.jetbrains:annotations from 26.0.0 to 26.0.1 (#399)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 26.0.0 to 26.0.1.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/26.0.0...26.0.1)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 01:34:29 +00:00
Jones cf27a9fe84 Exempt Bedrock players from the vehicle check 2024-10-15 20:10:13 +02:00
Jones bec6251b2a Exempt Bedrock players from the view distance check 2024-10-15 20:10:00 +02:00
Jones 6497b89f10 Use hashes for command tab completion 2024-10-15 19:29:20 +02:00
Jones 525d67808e Don't print permission message if empty 2024-10-15 19:11:54 +02:00
Jones 8864c0749a Optimize networking-related hot code 2024-10-15 12:35:23 +02:00
Jones 1ec4704f8e Fix configuration state race condition 2024-10-14 20:42:39 +02:00
Jones 8a4937e3af Fix error during configuration phase 2024-10-14 20:31:29 +02:00
dependabot[bot] f90446d4e3 Bump org.jetbrains:annotations from 25.0.0 to 26.0.0 (#396)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 25.0.0 to 26.0.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/25.0.0...26.0.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 01:14:01 +00:00
Jones 66141815cb Make verification world time configurable 2024-10-11 16:34:29 +02:00
dependabot[bot] 3fe7ccd65c Bump com.gradleup.shadow from 8.3.2 to 8.3.3 (#394)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.2 to 8.3.3.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.2...8.3.3)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 14:14:12 +02:00
dependabot[bot] f6203416d1 Bump io.netty:netty-all from 4.1.113.Final to 4.1.114.Final (#393)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.113.Final to 4.1.114.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.113.Final...netty-4.1.114.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 14:14:01 +02:00
Jones 3c73ecf60f Release 2.1.10 (#392) 2024-10-06 14:15:51 +02:00
Jones c116d7b556 Reduce the usage of static imports 2024-10-05 11:01:47 +02:00
Jones 057cda6c7a Remove VerifiedPlayerController#estimatedSize 2024-10-05 10:51:26 +02:00
Jones 3a97a1f949 Add easier API method for verifying players 2024-10-05 10:50:30 +02:00
Jones 69b20769de Add more stability checks to the database 2024-10-05 10:47:06 +02:00
Jones 9b87fde7c6 Implement missing view distance check 2024-10-05 09:38:13 +02:00
Jones b939767ab9 Fix translation error in Spanish translations 2024-10-01 17:04:13 +02:00
Jones d5a8cd5346 Tell user how to disable the welcome message 2024-10-01 17:03:58 +02:00
Jones aa4e4662bb Fix possible position issue with vehicles 2024-10-01 17:01:02 +02:00
dependabot[bot] 29187d4248 Bump org.jetbrains:annotations from 24.1.0 to 25.0.0 (#390)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.1.0 to 25.0.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/24.1.0...25.0.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:37:16 +02:00
Jones 0c8106eaf6 Release 2.1.9 (#389) 2024-09-29 13:32:05 +02:00
Jones 92ac5fbeb2 Fix block heights varying on different versions 2024-09-29 13:26:09 +02:00
Jones b800a2d154 Use one entity ID for all vehicles 2024-09-28 12:21:44 +02:00
Jones 1957463b81 Fix the Discord link in Polish translations 2024-09-28 11:59:09 +02:00
Jones 25114eabe5 Add verification welcome message to config 2024-09-28 11:58:54 +02:00
Jones a45b526ffb Clean up and improve the API 2024-09-25 17:52:35 +02:00
Jones 7b70d32e84 Don't store the pipeline separately in user 2024-09-25 17:48:51 +02:00
Jones e84c1a54db Small micro-optimizations to networking 2024-09-25 17:42:51 +02:00
Jones fd869ac199 Fix potential NullPointerException 2024-09-25 17:23:00 +02:00
dependabot[bot] abf1479a84 Bump com.gradleup.shadow from 8.3.1 to 8.3.2 (#387)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.1 to 8.3.2.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.1...8.3.2)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 15:03:49 +02:00
dependabot[bot] c00392e062 Bump bstats from 3.0.3 to 3.1.0 (#386)
Bumps `bstats` from 3.0.3 to 3.1.0.

Updates `org.bstats:bstats-bungeecord` from 3.0.3 to 3.1.0
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0)

Updates `org.bstats:bstats-bukkit` from 3.0.3 to 3.1.0
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0)

Updates `org.bstats:bstats-velocity` from 3.0.3 to 3.1.0
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-bungeecord
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.bstats:bstats-bukkit
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.bstats:bstats-velocity
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 15:03:32 +02:00
Jones fc9c1026b1 Release 2.1.8 (#385) 2024-09-22 13:21:19 +02:00
Jones aae49aeb63 Remove initial "unofficial branch" warning 2024-09-22 13:00:11 +02:00
Jones f7a6f04792 Make the update checker more accessible to users 2024-09-22 12:54:31 +02:00
Jones e36ba9840c Hide secure-chat toast from 1.20.5+ clients 2024-09-21 18:22:56 +02:00
Jones 40bceb71f1 Use the modern way of registering Velocity commands 2024-09-21 14:41:59 +02:00
Jones 2d3bf91444 Recode bandwidth statistic handling 2024-09-21 14:37:58 +02:00
Jones 45ab258a6f Fix upload-artifacts unable to find artifacts 2024-09-21 14:07:54 +02:00
Jones 600277660c General project base refactoring 2024-09-21 14:05:54 +02:00
MISHAandJones 6918afae52 JAR Downloads via GitHub Actions (#384)
* Update gradle.yml

* Update gradle.yml

* fix: smaller improvements to CI

* fix: wildcard pattern breaking CI

* fix(ci): revert wildcard patterns

---------

Co-authored-by: Jones <admin@jonesdev.xyz>
2024-09-21 13:45:15 +02:00
Jones 07cfc4ad1c Randomize X and Z positions in vehicle check 2024-09-19 17:30:06 +02:00
Jones 65eb9a503f Fix potential issue in the vehicle handler 2024-09-19 17:15:47 +02:00
Jones f5d7cc1511 Small collision check improvements 2024-09-19 16:59:25 +02:00
Jones 2cb0a48657 Add some more smaller movement validations 2024-09-19 16:45:39 +02:00
Jones 646f296039 Don't notify the channel about unhandled packets 2024-09-19 16:29:16 +02:00
Jones a698c26d39 Fix the 1.7.2-1.7.10 minimum Y coordinate issue 2024-09-19 16:21:19 +02:00
Jones 25136d6332 Small micro-optimizations in the packet registry 2024-09-19 13:11:20 +02:00
Jones ee4b1ffef4 Optimize processing of queued logins 2024-09-18 13:11:59 +02:00
Jones fe145836fe Use a better-suited data type for connected players 2024-09-18 12:34:34 +02:00
Jones 47100f4071 Bump Gradle from 8.10 to 8.10.1 2024-09-18 10:52:03 +02:00
Jones 57311996c8 Set Gradle groupId to xyz.jonesdev 2024-09-18 10:49:01 +02:00
Jones 7ad8e48431 Bump bungeecord-proxy from master-SNAPSHOT to 1.21-SNAPSHOT 2024-09-18 10:39:55 +02:00
Jones 154d999b88 Bump velocity-proxy from 3.3.0-SNAPSHOT to 3.4.0-SNAPSHOT 2024-09-18 10:39:04 +02:00
Jones bde637c9c5 Exempt Bedrock players from packet order checks 2024-09-18 10:07:56 +02:00
Jones cb1c62f478 Fix max total packets false positive on 1.7-1.8 2024-09-17 15:20:17 +02:00
Jones 3727742419 Add CaptchaGenerationStartEvent, CaptchaGenerationEndEvent 2024-09-17 14:52:15 +02:00
Jones f79bb38740 Remove a debug message 2024-09-17 14:48:47 +02:00
Jones 7d60ba85e7 Fix custom CAPTCHA background image not working 2024-09-17 14:44:31 +02:00
Jones cec7fa07c2 Fix another potential false positive with Geyser 2024-09-17 13:12:57 +02:00
Jones 2be2380e3f Fix invalid login mappings for 1.18-1.18.1 2024-09-16 14:18:08 +02:00
dependabot[bot] ae99ade03a Bump com.gradleup.shadow from 8.3.0 to 8.3.1 (#382)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 13:53:09 +02:00
Jones 59731a93f1 Release 2.1.7 (#381) 2024-09-15 14:09:43 +02:00
Jones ace55fbe7b Fix a small compilation error 2024-09-15 14:08:18 +02:00
Jones 696d3c5c81 Fix some Bedrock false positives 2024-09-15 14:03:00 +02:00
Jones a1e4ea3f5b Fix exception while serializing JsonArray (https://github.com/PaperMC/Velocity/pull/1426) 2024-09-14 15:45:19 +02:00
Jones 6b054bd56c Fix invalid block mappings for 1.14-1.14.3 2024-09-14 10:35:51 +02:00
Jones c8e2142dd9 Cap username length in LoginSuccess (clientbound) 2024-09-12 18:20:11 +02:00
Jones 1cf7ef6994 Add invalid window ID check to transactions 2024-09-11 21:14:08 +02:00
Jones af8aa103bf Randomize some more position-related values 2024-09-11 19:31:31 +02:00
Jones d1f2c15a8f Fix a false positive with the moved too far check 2024-09-11 19:28:29 +02:00
Jones 5a68c4d557 Added another vehicle Y position check 2024-09-11 19:23:58 +02:00
Jones 1438856531 Release 2.1.6 (#379) 2024-09-11 16:36:00 +02:00
AkkariinandJones 694725b798 Implement more interactionless checks (#378)
* Add minecart entity

* Add `VehicleMovePacket`

* Add `Animation` packet

* Add animation check.

* Fix the wrong entity minecart id for 1.21

* Add minecart for vehicle check.

* Exempt minecart check for 1.7-1.8 clients.

* Fix minecart id for lower than minecraft version 1.14.

* Fix wrong minecart entity id for 1.15

* Add missing license header

* Exempt animation check for 1.7.2 client due unknown disconnect error.

* style: small code style improvements

* style: even more style improvements

* refactor: inline some stuff

* style: small code cleanup

* fix: slot check not working in the first place

* style: don't use return; use an if-else block

* Fix animation check not working on 1.7.2+

* style: minor code style improvements

* fix: lombok compile error

* style: minor code style adjustments

* fix: optimize AnimationPacket

* fix: don't reassign unnecessary hand value

* style: code style fixes in *types

* style: add some comments to the *types

* [ci skip] style: small code style improvements

* Don't exempt 1.7-1.8 from the Minecart check

---------

Co-authored-by: Jones <admin@jonesdev.xyz>
2024-09-11 16:29:30 +02:00
Jones 47cb4d0840 Release 2.1.5 (#377) 2024-09-09 14:53:56 +02:00
dependabot[bot] 38ecb9ac06 Bump io.netty:netty-all from 4.1.112.Final to 4.1.113.Final (#376)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.112.Final to 4.1.113.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.112.Final...netty-4.1.113.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 14:31:43 +02:00
Jones 6657a3d674 Merge all custom logger wrappers into one 2024-09-08 18:32:35 +02:00
Jones 823ee21cea Clear up a small misconception in the code 2024-09-08 18:16:51 +02:00
Jones 4d206af0ae Don't allow players to send empty packets 2024-09-08 00:47:25 +02:00
Jones 2ff7b7efaa Fix gravity check false positive on 1.7 2024-09-07 23:19:10 +02:00
Jones a5efc1c796 Recode Y position and gravity calculation 2024-09-07 20:00:52 +02:00
Jones 85d5ce77b1 Implement SetPlayerRotationPacket packet order check 2024-09-07 19:34:46 +02:00
Jones 6c4d4b79d6 Implement PaddleBoatPacket packet order check 2024-09-07 19:34:19 +02:00
Jones 364b1c5b31 Optimize image RGB buffer writing 2024-09-07 14:48:55 +02:00
Jones 4edf04ede5 Randomize some placeholder variables during login 2024-09-07 12:53:24 +02:00
Jones 01da1a70ea Remove useless PlayerInfoPacket 2024-09-07 12:00:53 +02:00
Jones 9bb7c7cd60 Only log expired database entries if there are any 2024-09-07 11:20:12 +02:00
Jones aaa7588dcd Don't load the database driver twice 2024-09-07 11:19:06 +02:00
Jones 8d378d56ef Use regular JdbcConnectionSource, not pooled 2024-09-07 11:17:49 +02:00
Jones 476b1704b8 VarInt encoding optimizations (https://github.com/PaperMC/Velocity/commit/f034c0277d60e9f0a80e484832aec831cbc298f7) 2024-09-06 23:16:41 +02:00
Jones 26c31ceeaf VarInt reading optimizations (https://github.com/PaperMC/Velocity/pull/1418) 2024-09-06 23:07:26 +02:00
Jones 40f65ef5eb Rename 1.16(.2) codecs for more clarity 2024-09-06 22:54:50 +02:00
Jones 7736ccff86 Fix multiple block types being incorrect on 1.16 2024-09-06 22:53:24 +02:00
Jones 437a547ec3 [ci skip] Fix translation errors in copyright notice 2024-09-06 22:42:03 +02:00
Jones 4ec65218da [ci skip] Remove unused configuration entries 2024-09-06 22:31:15 +02:00
Jones de45e9f808 Mention TransferTool for Geyser in the config 2024-09-04 17:38:02 +02:00
Jones 87e768da0e Release 2.1.4 (#375) 2024-09-03 16:29:22 +02:00
Jones daa773bff1 General code style improvements and API fixes 2024-08-31 19:11:55 +02:00
Jones 1e43835d0b Quieten decoder exceptions to improve performance (#373)
* fix: use QuietDecoderException unless debug is enabled

* fix(bukkit): allow for more debug in the decoder
2024-08-31 13:57:41 +02:00
Jones efb8b3cd39 Don't handle the handshake for server pings 2024-08-31 13:49:32 +02:00
Jones 540899e92c Fix UserBlacklistedEvent not being called 2024-08-31 13:22:39 +02:00
Jones ef6140d47f Fix wrong usage for getting the blacklist score 2024-08-30 22:12:29 +02:00
Jones 27ca5a8368 Merge failCountCache and blacklist into one 2024-08-30 22:11:34 +02:00
Jones 1a43242c24 Clean up some inconsistent code style 2024-08-30 21:17:26 +02:00
Jones 65eeec6e17 Optimize login check order/priority 2024-08-30 21:17:14 +02:00
Jones d8e4dbd6cf Use host address instead of raw InetAddress 2024-08-30 21:12:03 +02:00
Jones 32973e674f Move ratelimiter implementations to the common module 2024-08-30 20:44:14 +02:00
Jones 05fa6de119 Fix some Java naming convention violations 2024-08-30 13:15:16 +02:00
Jones cf0f4e0b96 Code style improvements and micro-optimizations 2024-08-30 12:47:34 +02:00
Jones 0ae3c6c67b Remove unnecessary ctx.channel() calls 2024-08-30 12:37:25 +02:00
Jones aae564d16c Fix missing parentheses potentially causing problems 2024-08-29 18:38:16 +02:00
Jones f6338ba844 Recode the join rate-limiter 2024-08-29 18:29:48 +02:00
Jones d892645779 Reduce usage of channel.isActive() 2024-08-29 18:14:09 +02:00
Jones def40be5c0 Release 2.1.3 (#369) 2024-08-28 15:33:53 +02:00
Jones d4bdf86fcb Fix NullPointerException 2024-08-28 15:28:10 +02:00
264 changed files with 6951 additions and 6022 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[{*.java,*.kt}]
[*.java]
ij_java_wrap_long_lines = true
[{*.md,*.yml}]
+1
View File
@@ -0,0 +1 @@
patreon: jonesdev
+5
View File
@@ -48,6 +48,11 @@ Sonar also protects against spambot attacks since it queues the incoming connect
Sonar is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check.
If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues/new/choose) or a ticket on the [Discord server](https://jonesdev.xyz/discord/).
## Sponsors
Massive thanks to the sponsors of Sonar who help keep this project running:
<a href="https://github.com/Hydoxl"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/107579333?v=4&h=50&w=50&fit=cover&mask=circle&maxage=7d" alt="logo" align="center"></a>
## Building
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/development/building).
<br>
+35 -12
View File
@@ -9,9 +9,9 @@ name: Sonar CI
on:
push:
branches: [ "main", "dev" ]
branches: [ "main" ]
pull_request:
branches: [ "main", "dev" ]
branches: [ "main" ]
permissions:
contents: read
@@ -21,13 +21,36 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: build-sonar
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@v3
with:
arguments: build-sonar
- name: Upload Bukkit Artifact
uses: actions/upload-artifact@v4
with:
name: Sonar-Bukkit
path: bukkit/build/libs/Sonar-Bukkit.jar
if-no-files-found: error
- name: Upload BungeeCord Artifact
uses: actions/upload-artifact@v4
with:
name: Sonar-Bungee
path: bungeecord/build/libs/Sonar-Bungee.jar
if-no-files-found: error
- name: Upload Velocity Artifact
uses: actions/upload-artifact@v4
with:
name: Sonar-Velocity
path: velocity/build/libs/Sonar-Velocity.jar
if-no-files-found: error
+2 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) ${YEAR} Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,3 +14,4 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -9,5 +9,8 @@ dependencies {
compileOnly(rootProject.libs.annotations)
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
tasks {
shadowJar {
archiveFileName = "sonar-api-${rootProject.version}.jar"
}
}
@@ -0,0 +1,185 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
import java.text.DecimalFormat;
import java.util.UUID;
public interface Sonar {
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
/**
* Since we want to use Adventure on every server platform,
* we have to use their platform module to support BungeeCord and Bukkit.
*/
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
@ApiStatus.Internal
@NotNull Audience sender(final @NotNull Object object);
/**
* Gets the platform Sonar is running on (e.g., Bukkit, BungeeCord, Velocity).
*
* @return The {@link SonarPlatform} enum representing the current platform.
*/
@NotNull SonarPlatform getPlatform();
/**
* Gets the logger instance for Sonar.
*
* @return The {@link LoggerWrapper} instance.
*/
@NotNull LoggerWrapper getLogger();
/**
* Gets the Sonar configuration.
*
* @return The {@link SonarConfiguration} instance.
*/
@NotNull SonarConfiguration getConfig();
/**
* Gets the subcommand registry for Sonar commands.
*
* @return The {@link SubcommandRegistry} instance.
*/
@NotNull SubcommandRegistry getSubcommandRegistry();
/**
* Gets the controller for verified players.
* Used for managing players who have passed the verification.
*
* @return The {@link VerifiedPlayerController} instance.
*/
VerifiedPlayerController getVerifiedPlayerController();
/**
* Gets the handler for action bar notifications.
*
* @return The {@link ActionBarNotificationHandler} instance.
*/
@NotNull ActionBarNotificationHandler getActionBarNotificationHandler();
/**
* Gets the handler for chat notifications.
*
* @return The {@link ChatNotificationHandler} instance.
*/
@NotNull ChatNotificationHandler getChatNotificationHandler();
/**
* Gets the system timer used for tracking plugin launch time.
*
* @return The {@link SystemTimer} instance.
*/
@NotNull SystemTimer getLaunchTimer();
/**
* Gets the statistics tracker for Sonar.
*
* @return The {@link SonarStatistics} instance.
*/
@NotNull SonarStatistics getStatistics();
@SuppressWarnings("unused")
void setActionBarNotificationHandler(final @NotNull ActionBarNotificationHandler notificationHandler);
@SuppressWarnings("unused")
void setChatNotificationHandler(final @NotNull ChatNotificationHandler notificationHandler);
/**
* Reloads the Sonar configuration.
*/
void reload();
/**
* Gets the current version of Sonar.
*
* @return The {@link SonarVersion} instance.
*/
@NotNull default SonarVersion getVersion() {
return SonarVersion.INSTANCE;
}
/**
* Gets the event manager for Sonar.
*
* @return The {@link SonarEventManager} instance.
*/
@NotNull default SonarEventManager getEventManager() {
return SonarEventManager.INSTANCE;
}
/**
* Gets the attack tracker for Sonar.
*
* @return The {@link AttackTracker} instance.
*/
@NotNull default AttackTracker getAttackTracker() {
return AttackTracker.INSTANCE;
}
/**
* Gets the anti-bot handler (called "Fallback") for Sonar.
*
* @return The {@link Fallback} instance.
*/
@NotNull default Fallback getFallback() {
return Fallback.INSTANCE;
}
/**
* Gets the instance of the Sonar API.
*
* @return The {@link Sonar} instance.
* @throws IllegalStateException If Sonar has not been initialized.
*/
@NotNull static Sonar get() {
return SonarSupplier.get();
}
/**
* Gets the instance of the Sonar API.
* Only use this method if you understand the implications,
* and have a specific reason to bypass the standard {@link #get()} method.
* In most cases, {@link #get()} is the preferred way to access the Sonar API.
*
* @return The Sonar instance.
*/
@ApiStatus.Internal
@NotNull static Sonar get0() {
return SonarSupplier.sonar;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,21 +26,15 @@ import java.util.function.Function;
@Getter
@RequiredArgsConstructor
public enum SonarPlatform {
BUKKIT("Bukkit", 19110,
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
pipeline -> "packet_handler"),
BUNGEE("BungeeCord", 19109,
pipeline -> "packet-encoder",
pipeline -> "inbound-boss"),
VELOCITY("Velocity", 19107,
pipeline -> "minecraft-encoder",
pipeline -> "handler");
BUKKIT("Bukkit", 19110, "packet_handler",
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder"),
BUNGEE("BungeeCord", 19109, "inbound-boss",
pipeline -> "packet-encoder"),
VELOCITY("Velocity", 19107, "handler",
pipeline -> "minecraft-encoder");
private final String displayName;
/**
* bStats service ID for the respective Sonar platform
*/
private final int metricsId;
private final Function<ChannelPipeline, String> encoderFunction;
private final Function<ChannelPipeline, String> handlerFunction;
private final String connectionHandler;
private final Function<ChannelPipeline, String> encoder;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,20 +20,21 @@ package xyz.jonesdev.sonar.api;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@UtilityClass
public class SonarSupplier {
private Sonar sonar;
Sonar sonar;
public void set(final @NotNull Sonar s) {
public void set(final @NotNull Sonar _sonar) {
if (sonar != null) {
throw new AlreadyInitializedException();
throw new IllegalStateException("Sonar is already initialized");
}
sonar = Objects.requireNonNull(s);
sonar = _sonar;
}
public @NotNull Sonar get() {
public Sonar get() {
if (sonar == null) {
throw new IllegalStateException("Sonar is not initialized yet");
}
return sonar;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,7 +42,6 @@ public final class SonarVersion {
this.formatted = version + " (" + gitCommit + ")";
}
// Taken from
// https://github.com/PaperMC/Velocity/pull/1336/
private static @NotNull Manifest getManifest() {
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,6 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@@ -34,23 +33,12 @@ public final class InvocationSource {
private final Predicate<String> permissionFunction;
/**
* @return True, if {@link InvocationSource#uuid} is not null
* @apiNote This indicates a player as a player will always have a UUID
* @return true if {@link InvocationSource#uuid} is not null
*/
public boolean isPlayer() {
return uuid != null;
}
/**
* Sends a message to the command executor
*
* @apiNote We should probably use cached components...
* (See {@link InvocationSource#sendMessage(Component)})
*/
public void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,20 +26,19 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.update.UpdateChecker;
import java.util.*;
import static java.util.Collections.emptyList;
public interface SonarCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
Map<Integer, List<String>> TAB_SUGGESTIONS = new LinkedHashMap<>();
default void handle(final @NotNull InvocationSource source, final String @NotNull [] args) {
// Check if the player actually has the permission to run the command
if (source.isPlayer() && !source.getPermissionFunction().test("sonar.command")) {
source.sendMessage(Sonar.get().getConfig().getNoPermission());
if (Sonar.get0().getConfig().getNoPermission() != null) {
source.sendMessage(Sonar.get0().getConfig().getNoPermission());
}
return;
}
@@ -47,7 +46,7 @@ public interface SonarCommand {
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
subcommand = Sonar.get0().getSubcommandRegistry().getSubcommands().stream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
@@ -58,30 +57,37 @@ public interface SonarCommand {
if (!command.getInfo().onlyConsole()
&& !source.getPermissionFunction().test(command.getPermission())) {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.subcommand-no-permission"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Sonar.get0().getConfig().getMessagesConfig().getString("commands.subcommand-no-permission"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix()),
Placeholder.unparsed("permission", command.getPermission())));
return;
}
// Invoke subcommand
command.invoke(source, args);
}, () -> {
final UpdateChecker.CheckResult checkResult = UpdateChecker.getLastCheckResult();
Component version = Component.text(Sonar.get0().getVersion().getFormatted());
if (checkResult.getConfigKey() != null) {
version = version.append(MiniMessage.miniMessage().deserialize(
Sonar.get0().getConfig().getMessagesConfig().getString("commands.main." + checkResult.getConfigKey())));
}
// Print standard help message
source.sendMessage(MiniMessage.miniMessage().deserialize(
String.join("<newline>",
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
Sonar.get0().getConfig().getMessagesConfig().getStringList("commands.main.header")),
Placeholder.component("version", version),
Placeholder.unparsed("platform", Sonar.get0().getPlatform().getDisplayName()),
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
final Component yes = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.tick"));
Sonar.get0().getConfig().getMessagesConfig().getString("commands.main.tick"));
final Component no = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.cross"));
Sonar.get0().getConfig().getMessagesConfig().getString("commands.main.cross"));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(command -> {
Sonar.get0().getSubcommandRegistry().getSubcommands().forEach(command -> {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
Sonar.get0().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
TagResolver.resolver("suggest-subcommand",
(queue, context) -> Tag.styling(ClickEvent.suggestCommand("/sonar " + command.getInfo().name()))),
Placeholder.unparsed("subcommand", command.getInfo().name()),
@@ -94,30 +100,29 @@ public interface SonarCommand {
});
}
static void prepareCachedMessages() {
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), arguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, arguments);
static void prepareCachedTabSuggestions() {
if (TAB_SUGGESTIONS.isEmpty()) {
final List<String> subcommandsAndAliases = new ArrayList<>();
for (final Subcommand subcommand : Sonar.get0().getSubcommandRegistry().getSubcommands()) {
subcommandsAndAliases.add(subcommand.getInfo().name());
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
TAB_SUGGESTIONS.put(subcommand.getInfo().name().hashCode(), arguments);
for (final String alias : subcommand.getInfo().aliases()) {
subcommandsAndAliases.add(alias);
TAB_SUGGESTIONS.put(alias.hashCode(), arguments);
}
}
TAB_SUGGESTIONS.put(-1, subcommandsAndAliases);
}
}
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
if (arguments.length <= 1) {
return TAB_SUGGESTIONS;
} else if (arguments.length == 2) {
final String subcommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subcommandName, emptyList());
if (arguments.length <= 2) {
final int subcommandHash = arguments.length <= 1 ? -1 : arguments[0].toLowerCase().hashCode();
return TAB_SUGGESTIONS.getOrDefault(subcommandHash, Collections.emptyList());
}
return emptyList();
return Collections.emptyList();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import java.util.Objects;
@@ -49,8 +48,8 @@ public abstract class Subcommand {
protected static @Nullable String validateIP(final InvocationSource source, final String raw) {
if (!IPv4_REGEX.matcher(raw).matches() && !IPv6_REGEX.matcher(raw).matches()) {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.invalid-ip-address"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
Sonar.get0().getConfig().getMessagesConfig().getString("commands.invalid-ip-address"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix())));
return null;
}
return raw;
@@ -62,8 +61,8 @@ public abstract class Subcommand {
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource, final String usage) {
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.incorrect-usage"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Sonar.get0().getConfig().getMessagesConfig().getString("commands.incorrect-usage"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix()),
Placeholder.unparsed("subcommand-usage", usage)));
}
@@ -71,16 +70,16 @@ public abstract class Subcommand {
// Check if the subcommand can only be executed by players
if (info.onlyPlayers() && !invocationSource.isPlayer()) {
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.player-only"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
Sonar.get0().getConfig().getMessagesConfig().getString("commands.player-only"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix())));
return;
}
// Check if the subcommand can only be executed though console
if (info.onlyConsole() && invocationSource.isPlayer()) {
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.console-only"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
Sonar.get0().getConfig().getMessagesConfig().getString("commands.console-only"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix())));
return;
}
@@ -91,13 +90,13 @@ public abstract class Subcommand {
}
// Execute the sub command from the invocation source with the given arguments
execute(new CommandInvocation(invocationSource, arguments));
execute(invocationSource, arguments);
}
public final @NotNull String getDescription() {
final String path = String.format("commands.%s.description", info.name());
return Objects.requireNonNull(Sonar.get().getConfig().getMessagesConfig().getString(path));
return Objects.requireNonNull(Sonar.get0().getConfig().getMessagesConfig().getString(path));
}
protected abstract void execute(final @NotNull CommandInvocation invocation);
protected abstract void execute(final @NotNull InvocationSource source, final String @NotNull [] args);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,34 +25,15 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SubcommandInfo {
/**
* @return Name of the subcommand
*/
String name();
/**
* @return Aliases of the subcommand
*/
String[] aliases() default {};
/**
* @return Arguments of the subcommand
*/
String[] arguments() default {};
/**
* @return Whether the subcommands requires arguments
*/
boolean argumentsRequired() default true;
/**
* @return Whether the subcommands can only be executed by a player
*/
boolean onlyPlayers() default false;
/**
* @return Whether the subcommands can only be executed by console
*/
boolean onlyConsole() default false;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -140,6 +140,7 @@ public enum Language {
FA("Persian"),
PL("Polish"),
PT("Portuguese"),
PT_BR("Brazilian Portuguese", "pt-br"),
PS("Pushto"),
QU("Quechua"),
RM("Raeto-Romance"),
@@ -203,7 +204,18 @@ public enum Language {
this.code = name().toLowerCase();
}
Language(final String name, final String code) {
this.name = name;
this.code = code;
}
public static Language fromCode(final @NotNull String code) {
return Language.valueOf(code.toUpperCase());
for (final Language language : values()) {
if (language.code.equalsIgnoreCase(code)) {
return language;
}
}
// Fallback to English
return EN;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.MemorySection;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.File;
import java.io.InputStream;
@@ -31,8 +32,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
@Getter
public final class SimpleYamlConfig {
private final File file;
@@ -90,7 +89,7 @@ public final class SimpleYamlConfig {
public int getInt(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get0().getLogger().warn("Could not find {} in {}.", path, file.getName());
return 0;
}
return yaml.getInt(path);
@@ -98,7 +97,7 @@ public final class SimpleYamlConfig {
public boolean getBoolean(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get0().getLogger().warn("Could not find {} in {}.", path, file.getName());
return false;
}
return yaml.getBoolean(path);
@@ -106,7 +105,7 @@ public final class SimpleYamlConfig {
public @NotNull String getString(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get0().getLogger().warn("Could not find {} in {}.", path, file.getName());
return "";
}
final Object object = yaml.get(path);
@@ -118,7 +117,7 @@ public final class SimpleYamlConfig {
public List<String> getStringList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get0().getLogger().warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getStringList(path);
@@ -126,7 +125,7 @@ public final class SimpleYamlConfig {
public List<Integer> getIntList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get0().getLogger().warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getIntegerList(path);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,10 +27,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -52,24 +51,6 @@ public final class SonarConfiguration {
@Getter
private Language language;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[config] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[config] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[config] " + message, args);
}
};
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
this.messagesConfig = new SimpleYamlConfig(new File(pluginFolder, "messages.yml"));
@@ -85,14 +66,14 @@ public final class SonarConfiguration {
final URL defaultLanguageFile = Sonar.class.getResource("/assets/language.properties");
// Make sure the file actually exists before trying to copy it
if (defaultLanguageFile == null) {
LOGGER.error("Cannot check for custom language (is the file missing?)");
Sonar.get0().getLogger().error("Cannot check for custom language (is the file missing?)");
return DEFAULT_FALLBACK_LANGUAGE;
}
// Copy the file to the plugin data directory
try (final InputStream inputStream = defaultLanguageFile.openStream()) {
Files.copy(inputStream, languageFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException exception) {
LOGGER.error("Error copying file: {}", exception);
Sonar.get0().getLogger().error("Error copying file: {}", exception);
return DEFAULT_FALLBACK_LANGUAGE;
}
}
@@ -106,13 +87,13 @@ public final class SonarConfiguration {
// Try parsing the property as a language
return Language.fromCode(property);
} catch (Throwable throwable) {
LOGGER.error("Could not find requested language: {}", throwable);
LOGGER.error("You can view a full list of valid language codes here:");
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
Sonar.get0().getLogger().error("Could not find requested language: {}", throwable);
Sonar.get0().getLogger().error("You can view a full list of valid language codes here:");
Sonar.get0().getLogger().error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
Sonar.get0().getLogger().error("If a translation does not exist yet, Sonar will use English (en).");
}
} catch (IOException exception) {
LOGGER.error("Error reading language file: {}", exception);
Sonar.get0().getLogger().error("Error reading language file: {}", exception);
}
return DEFAULT_FALLBACK_LANGUAGE;
}
@@ -131,13 +112,13 @@ public final class SonarConfiguration {
final String property = System.getProperty("user.language", "en");
language = Language.fromCode(property);
// Make sure the user knows that we're using the system language for translations
LOGGER.info("Using system language ({}) for translations.", language);
Sonar.get0().getLogger().info("Using system language ({}) for translations.", language);
} catch (Exception exception) {
LOGGER.warn("Could not use system language for translations.");
LOGGER.warn("Using default language ({}) for translations.", language);
Sonar.get0().getLogger().warn("Could not use system language for translations.");
Sonar.get0().getLogger().warn("Using default language ({}) for translations.", language);
}
} else {
LOGGER.info("Using custom language ({}) for translations.", language);
Sonar.get0().getLogger().info("Using custom language ({}) for translations.", language);
}
// Load all configurations
@@ -177,12 +158,12 @@ public final class SonarConfiguration {
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
// Warn the user if the verification timing is set to NEVER
if (verification.timing == Verification.Timing.NEVER) {
LOGGER.warn(" ");
LOGGER.warn("You have set the verification timing to 'NEVER'.");
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
LOGGER.warn("Please only edit this option if you really know what you are doing.");
LOGGER.warn(" ");
Sonar.get0().getLogger().warn(" ");
Sonar.get0().getLogger().warn("You have set the verification timing to 'NEVER'.");
Sonar.get0().getLogger().warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
Sonar.get0().getLogger().warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
Sonar.get0().getLogger().warn("Please only edit this option if you really know what you are doing.");
Sonar.get0().getLogger().warn(" ");
}
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
@@ -202,10 +183,11 @@ public final class SonarConfiguration {
final String backgroundPath = generalConfig.getString("verification.checks.map-captcha.background");
if (!backgroundPath.isEmpty()) {
try (final InputStream inputStream = new FileInputStream(backgroundPath)) {
verification.map.backgroundImage = inputStream;
} catch (IOException exception) {
Sonar.get().getLogger().error("Could not find background image", exception);
final File backgroundFile = new File(pluginFolder, backgroundPath);
if (backgroundFile.exists()) {
verification.map.backgroundImage = backgroundFile;
} else {
Sonar.get0().getLogger().error("Could not find background image {}", backgroundFile.getAbsolutePath());
}
}
@@ -213,10 +195,10 @@ public final class SonarConfiguration {
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
verification.brand.maxLength = generalConfig.getInt("verification.checks.client-brand.max-length");
verification.timeOfDay = clamp(generalConfig.getInt("verification.time-of-day"), 0, 24000);
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
@@ -238,16 +220,12 @@ public final class SonarConfiguration {
webhook.content = webhookConfig.getString("webhook.content");
webhook.footer.text = webhookConfig.getString("webhook.embed.footer.text");
webhook.footer.iconUrl = webhookConfig.getString("webhook.embed.footer.icon-url");
final String realEmbedPath = "webhook.embed";
final String embedPath = realEmbedPath + ".";
webhook.embed.title = webhookConfig.getString(embedPath + "title");
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
webhook.embed.description = String.join("\n", webhookConfig.getStringList(embedPath + "description"));
webhook.embed.r = webhookConfig.getInt(embedPath + "color.red");
webhook.embed.g = webhookConfig.getInt(embedPath + "color.green");
webhook.embed.b = webhookConfig.getInt(embedPath + "color.blue");
webhook.embed.title = webhookConfig.getString("webhook.embed.title");
webhook.embed.titleUrl = webhookConfig.getString("webhook.embed.title-url");
webhook.embed.description = String.join("\n", webhookConfig.getStringList("webhook.embed.description"));
webhook.embed.r = webhookConfig.getInt("webhook.embed.color.red");
webhook.embed.g = webhookConfig.getInt("webhook.embed.color.green");
webhook.embed.b = webhookConfig.getInt("webhook.embed.color.blue");
if (!webhook.url.isEmpty()) {
if (webhook.username.isEmpty()) {
@@ -261,10 +239,16 @@ public final class SonarConfiguration {
// Messages
prefix = MiniMessage.miniMessage().deserialize(messagesConfig.getString("prefix"));
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
Placeholder.component("prefix", prefix));
SonarCommand.prepareCachedMessages();
final String noPermissionMessage = messagesConfig.getString("commands.no-permission");
if (noPermissionMessage.isEmpty()) {
noPermission = null;
} else {
noPermission = MiniMessage.miniMessage().deserialize(noPermissionMessage,
Placeholder.component("prefix", prefix));
}
SonarCommand.prepareCachedTabSuggestions();
supportUrl = messagesConfig.getString("support-url");
header = MiniMessage.miniMessage().deserialize(
@@ -278,6 +262,7 @@ public final class SonarConfiguration {
tooManyOnlinePerIp = deserializeDisconnectMessage("too-many-online-per-ip");
verification.currentlyPreparing = deserializeDisconnectMessage("verification.currently-preparing");
verification.unsupportedVersion = deserializeDisconnectMessage("verification.unsupported-version");
verification.tooFastReconnect = deserializeDisconnectMessage("verification.too-fast-reconnect");
verification.alreadyVerifying = deserializeDisconnectMessage("verification.already-verifying");
verification.alreadyQueued = deserializeDisconnectMessage("verification.already-queued");
@@ -303,7 +288,7 @@ public final class SonarConfiguration {
final String resourceName = url + "/" + language.getCode() + ".yml";
URL result = Sonar.class.getResource("/assets/" + resourceName);
if (result == null) {
LOGGER.warn("Could not find " + resourceName + "! Using en.yml!");
Sonar.get0().getLogger().warn("Could not find " + resourceName + "! Using en.yml!");
result = Objects.requireNonNull(Sonar.class.getResource("/assets/" + url + "/en.yml"));
}
return result;
@@ -312,13 +297,13 @@ public final class SonarConfiguration {
private static int clamp(final int v, final int max, final int min) {
final int output = Math.max(Math.min(v, min), max);
if (output != v) {
Sonar.get().getLogger().warn("Clamped configuration value {} to {}", v, output);
Sonar.get0().getLogger().warn("Clamped configuration value {} to {}", v, output);
}
return output;
}
public String formatAddress(final InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
public @NotNull String formatAddress(final @NotNull InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.getHostAddress() : "<ip address withheld>";
}
@Getter
@@ -386,7 +371,7 @@ public final class SonarConfiguration {
private int maxDuration;
private int maxTries;
private String alphabet;
private InputStream backgroundImage;
private File backgroundImage;
}
@Getter
@@ -427,6 +412,7 @@ public final class SonarConfiguration {
}
}
private int timeOfDay;
private boolean checkGeyser;
private boolean logConnections;
private boolean logDuringAttack;
@@ -434,7 +420,6 @@ public final class SonarConfiguration {
private Pattern validNameRegex;
private Pattern validLocaleRegex;
private int maxLoginPackets;
private int readTimeout;
private int writeTimeout;
private int reconnectDelay;
@@ -452,6 +437,7 @@ public final class SonarConfiguration {
private Component blacklisted;
private Component protocolBlacklisted;
private Component currentlyPreparing;
private Component unsupportedVersion;
}
@Getter
@@ -489,7 +475,7 @@ public final class SonarConfiguration {
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
private boolean loaded;
}
private Type type;
@@ -0,0 +1,248 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.database.controller;
import com.alessiodp.libby.LibraryManager;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
import java.io.File;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
@Getter
private final Set<String> cache = Collections.synchronizedSet(new HashSet<>());
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@Getter
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
private final ExecutorService updateService = Executors.newSingleThreadExecutor();
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
final SonarConfiguration.Database database = Sonar.get0().getConfig().getDatabase();
cachedDatabaseType = database.getType();
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
Sonar.get0().getLogger().warn("Configure a database to save verified players.");
return;
}
// Make sure to only load the driver once per database type
if (!cachedDatabaseType.isLoaded()) {
Sonar.get0().getLogger().info("Loading {} driver version {}",
cachedDatabaseType.getDatabaseType().getDatabaseName(),
cachedDatabaseType.getDatabaseDriver().getVersion());
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setLoaded(true);
}
try {
final String jdbcURL;
// H2 has a different JDBC URL layout
// https://www.codejava.net/java-se/jdbc/connect-to-h2-database-examples
if (cachedDatabaseType == SonarConfiguration.Database.Type.H2) {
final File file = new File(Sonar.get0().getConfig().getPluginFolder(),
Sonar.get0().getConfig().getGeneralConfig().getString("database.filename"));
jdbcURL = String.format(cachedDatabaseType.getConnectionString(), file.getAbsolutePath());
} else {
// Normal JDBC URL layout for MySQL/MariaDB/...
jdbcURL = String.format(cachedDatabaseType.getConnectionString(),
Sonar.get0().getConfig().getGeneralConfig().getString("database.host"),
Sonar.get0().getConfig().getGeneralConfig().getInt("database.port"),
Sonar.get0().getConfig().getGeneralConfig().getString("database.name"));
}
connectionSource = new JdbcConnectionSource(jdbcURL,
Sonar.get0().getConfig().getGeneralConfig().getString("database.username"),
Sonar.get0().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException ignored) {
/*
* This is caused by a duplicate index;
* I know this isn't the best method of handling it,
* but I don't know how else I could address this issue.
*/
}
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
queryBuilder = dao.queryBuilder();
// Make sure to run the clean task and the caching task in the same thread
// https://github.com/jonesdevelopment/sonar/issues/150
updateService.execute(() -> {
if (connectionSource != null) {
try {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(verifiedPlayer -> cache.add(verifiedPlayer.getFingerprint()));
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
}
});
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
}
/**
* Closes the connection if instantiated
*/
public void close() {
// The connection source will always be null if the database type is NONE.
if (connectionSource != null) {
// Shut down the update service before letting it re-use the connection.
updateService.shutdown();
try {
connectionSource.close();
} catch (Exception exception) {
exception.printStackTrace(System.err);
}
// Make sure not to reuse a closed connection
connectionSource = null;
}
}
/**
* Clear all old entries using the given timestamp
*/
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
final long timestamp = Instant.now().minus(maximumAge, ChronoUnit.DAYS).getEpochSecond() * 1000L;
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null && !oldEntries.isEmpty()) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
Sonar.get0().getLogger().info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
/**
* First, remove the player from the local cache and then,
* secondly, asynchronously add the player to the database.
*
* @param fingerprint Fingerprint of the verified player
*/
public void remove(final @NotNull String fingerprint) {
cache.remove(fingerprint);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
updateService.execute(() -> {
if (connectionSource != null) {
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
.eq("fingerprint", fingerprint)
.query();
if (verifiedPlayer != null) {
for (final VerifiedPlayer player : verifiedPlayer) {
dao.delete(player);
}
}
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
}
});
}
/**
* Creates a new VerifiedPlayer model from the given username, host address, and timestamp
*/
public void add(final @NotNull String username, final @NotNull String hostAddress, final long timestamp) {
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
// Add a new VerifiedPlayer object from the given parameters to the database
add(new VerifiedPlayer(fingerprint, timestamp));
}
/**
* Caches the player locally and then adds the player to the database
*/
public void add(final @NotNull VerifiedPlayer player) {
cache.add(player.getFingerprint());
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
updateService.execute(() -> {
if (connectionSource != null) {
try {
dao.createIfNotExists(player);
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
}
});
}
/**
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
*/
public void clearAll() {
cache.clear();
// Delete the entire table from the database, if necessary
if (connectionSource != null
&& cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.model;
package xyz.jonesdev.sonar.api.database.model;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@@ -25,34 +25,23 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
import java.sql.Timestamp;
import java.util.UUID;
@Getter
@ToString
@DatabaseTable(tableName = "sonar_fingerprints")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DatabaseTable(tableName = "sonar_verified_players")
public final class VerifiedPlayer {
@SuppressWarnings("unused")
@DatabaseField(generatedId = true)
private int id;
@DatabaseField(
columnName = "ip_address",
columnName = "fingerprint",
canBeNull = false,
uniqueIndexName = "ip_address_player_uuid_idx",
width = 16
width = 48
)
private String inetAddress;
@DatabaseField(
columnName = "player_uuid",
canBeNull = false,
uniqueIndexName = "ip_address_player_uuid_idx",
width = 36
)
private UUID playerUuid;
private String fingerprint;
@DatabaseField(
columnName = "timestamp",
@@ -60,17 +49,9 @@ public final class VerifiedPlayer {
)
private Timestamp timestamp;
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
final @NotNull UUID playerUuid,
public VerifiedPlayer(final @NotNull String fingerprint,
final long timestamp) {
this(inetAddress.toString(), playerUuid, timestamp);
}
public VerifiedPlayer(final @NotNull String inetAddress,
final @NotNull UUID playerUuid,
final long timestamp) {
this.inetAddress = inetAddress;
this.playerUuid = playerUuid;
this.fingerprint = fingerprint;
this.timestamp = new Timestamp(timestamp);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.H2DatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,13 +17,13 @@
package xyz.jonesdev.sonar.api.event;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
public interface SonarEventListener {
/**
* Handles the publishing of an event
*
* @param event [Cancellable] Sonar event
* Handles the publishing of an event through Sonar's API
*/
void handle(final SonarEvent event);
void handle(final @NotNull SonarEvent event);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,12 +35,9 @@ public final class SonarEventManager {
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
private static final ExecutorService EVENT_SERVICE = Executors.newSingleThreadExecutor();
/**
* Publishes the given event to all listeners
*/
@ApiStatus.Internal
public void publish(final @NotNull SonarEvent event) {
// Don't post a task if there are no listeners
// Don't post an event if there are no listeners
if (EVENT_LISTENERS.isEmpty()) {
return;
}
@@ -50,7 +47,7 @@ public final class SonarEventManager {
try {
eventListener.handle(event);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not pass {} to listener: {}",
Sonar.get0().getLogger().error("Could not pass {} to listener: {}",
event.getClass().getSimpleName(), throwable);
}
}
@@ -61,11 +58,13 @@ public final class SonarEventManager {
* Registers one (or more) event listeners
*
* @param listeners One (or more) listeners to register
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
* @see #unregisterListener(SonarEventListener[]) Unregister an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
public void registerListener(final @NotNull SonarEventListener... listeners) {
synchronized (EVENT_LISTENERS) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
}
}
/**
@@ -73,10 +72,12 @@ public final class SonarEventManager {
*
* @param listeners One (or more) listeners to unregister
* @apiNote This does not have any effect if the given listeners are not registered
* @see #registerListener(SonarEventListener...) Register an event listener
* @see #registerListener(SonarEventListener[]) Register an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
public void unregisterListener(final @NotNull SonarEventListener... listeners) {
synchronized (EVENT_LISTENERS) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,8 +20,8 @@ package xyz.jonesdev.sonar.api.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
@Getter
@ToString
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,16 +15,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.api.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@Getter
@ToString
@RequiredArgsConstructor
@ToString(of = "rawArguments")
public final class CommandInvocation {
private final InvocationSource source;
private final String[] rawArguments;
public final class CaptchaGenerationEndEvent implements SonarEvent {
private final SystemTimer timer;
private final int amountGenerated;
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event.impl;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
@Getter
@Setter
@AllArgsConstructor
public final class CaptchaGenerationStartEvent implements SonarEvent {
private CaptchaGenerator captchaGenerator;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.api.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.ApiStatus;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@@ -28,6 +27,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@ToString
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
private final FallbackUser user;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@ToString
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
private final FallbackUser user;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,14 +23,9 @@ import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.UUID;
@Getter
@ToString
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser user;
private final long timeTakenToVerify;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,11 +22,10 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import java.net.InetAddress;
import java.util.concurrent.ConcurrentHashMap;
@@ -37,50 +36,29 @@ import java.util.concurrent.ConcurrentMap;
public final class Fallback {
public static final Fallback INSTANCE = new Fallback();
// Map of all players connected to the server in general
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
// Map of all connected IP addresses (used for fast checking)
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
private final ConcurrentMap<InetAddress, Boolean> connected = new ConcurrentHashMap<>(256);
private final FallbackLoginQueue queue = new FallbackLoginQueue();
@Setter
private Cache<InetAddress, Byte> blacklist;
private Cache<String, Integer> blacklist;
@Setter
private long blacklistTime;
@Setter
private CaptchaGenerator captchaGenerator;
private final @NotNull FallbackQueue queue = new FallbackQueue();
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
private final LoggerWrapper logger = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[fallback] " + message, args);
}
};
@Setter
private Ratelimiter<InetAddress> ratelimiter;
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
return shouldPerform(Sonar.get0().getConfig().getVerification().getTiming());
}
public boolean shouldPerformCaptcha() {
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
return shouldPerform(Sonar.get0().getConfig().getVerification().getMap().getTiming());
}
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
&& Sonar.get().getAttackTracker().getCurrentAttack() != null);
&& Sonar.get0().getAttackTracker().getCurrentAttack() != null);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,8 +23,6 @@ import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.Sonar;
import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
@@ -32,27 +30,24 @@ import java.util.concurrent.Executors;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackQueue {
public final class FallbackLoginQueue {
// Fixed thread pool executor for all new verifications
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
128, 0.75f, Runtime.getRuntime().availableProcessors());
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(512);
public void poll() {
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// Iterate through the map and process up to maxQueuePolls entries
final int maxQueuePolls = Math.min(players.size(), Sonar.get0().getConfig().getQueue().getMaxQueuePolls());
// No need to initiate an executor service task if nobody is currently queued
if (maxQueuePolls <= 0) return;
// We need to be cautious here since we don't want any concurrency issues
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = players.entrySet().iterator();
while (iterator.hasNext() && index++ < maxQueuePolls) {
// Run the cached runnable
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
QUEUE_EXECUTOR.execute(entry.getValue());
// Remove runnable from the map
iterator.remove();
}
QUEUE_EXECUTOR.execute(() -> {
final var iterator = players.entrySet().iterator();
for (int index = 0; index < maxQueuePolls && iterator.hasNext(); index++) {
iterator.next().getValue().run();
iterator.remove();
}
});
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
@@ -26,34 +25,23 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.net.InetAddress;
import java.util.UUID;
public interface FallbackUser {
@NotNull
Channel getChannel();
@NotNull Channel channel();
@NotNull
ChannelPipeline getPipeline();
@NotNull InetAddress getInetAddress();
@NotNull
InetAddress getInetAddress();
@NotNull ProtocolVersion getProtocolVersion();
@NotNull
ProtocolVersion getProtocolVersion();
@NotNull SystemTimer getLoginTimer();
@NotNull
SystemTimer getLoginTimer();
@NotNull String getFingerprint();
@NotNull
UUID getOfflineUuid();
@NotNull String getUsername();
boolean isReceivedClientSettings();
boolean isForceCaptcha();
void setReceivedClientSettings(final boolean receivedClientSettings);
boolean isReceivedPluginMessage();
void setReceivedPluginMessage(final boolean receivedPluginMessage);
void setForceCaptcha(final boolean isForceCaptcha);
boolean isGeyser();
@@ -65,22 +53,14 @@ public interface FallbackUser {
*/
void disconnect(final @NotNull Component reason);
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
*
* @param msg Message to send to the player
*/
default void write(final @NotNull Object msg) {
if (getChannel().isActive()) {
getChannel().writeAndFlush(msg, getChannel().voidPromise());
if (channel().isActive()) {
channel().writeAndFlush(msg, channel().voidPromise());
} else {
ReferenceCountUtil.release(msg);
}
@@ -91,22 +71,10 @@ public interface FallbackUser {
* sent once all messages are flushed.
*/
default void delayedWrite(final @NotNull Object msg) {
if (getChannel().isActive()) {
getChannel().write(msg, getChannel().voidPromise());
if (channel().isActive()) {
channel().write(msg, channel().voidPromise());
} else {
ReferenceCountUtil.release(msg);
}
}
/**
* Disconnects the player who failed the verification
* and caches them in FAILED_VERIFICATIONS.
* If the player fails the verification twice,
* the player will be temporarily denied from verifying.
*
* @param reason Reason for failing the verification
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
* will not be thrown if no reason is given
*/
void fail(final @NotNull String reason);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,9 +17,15 @@
package xyz.jonesdev.sonar.api.fallback.captcha;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
@FunctionalInterface
public interface CaptchaGenerator {
BufferedImage createImage(final char[] answer);
/**
* Generates a {@link java.awt.image.BufferedImage} that shows the answer to the CAPTCHA
*/
@NotNull BufferedImage createImage(final char @NotNull [] answer);
}
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Getter
@RequiredArgsConstructor
public enum ProtocolVersion {
UNKNOWN(-1, "Unknown"),
MINECRAFT_1_7_2(4, "1.7.2"),
MINECRAFT_1_7_6(5, "1.7.6"),
MINECRAFT_1_8(47, "1.8"),
MINECRAFT_1_9(107, "1.9"),
MINECRAFT_1_9_1(108, "1.9.1"),
MINECRAFT_1_9_2(109, "1.9.2"),
MINECRAFT_1_9_4(110, "1.9.3"),
MINECRAFT_1_10(210, "1.10"),
MINECRAFT_1_11(315, "1.11"),
MINECRAFT_1_11_1(316, "1.11.1"),
MINECRAFT_1_12(335, "1.12"),
MINECRAFT_1_12_1(338, "1.12.1"),
MINECRAFT_1_12_2(340, "1.12.2"),
MINECRAFT_1_13(393, "1.13"),
MINECRAFT_1_13_1(401, "1.13.1"),
MINECRAFT_1_13_2(404, "1.13.2"),
MINECRAFT_1_14(477, "1.14"),
MINECRAFT_1_14_1(480, "1.14.1"),
MINECRAFT_1_14_2(485, "1.14.2"),
MINECRAFT_1_14_3(490, "1.14.3"),
MINECRAFT_1_14_4(498, "1.14.4"),
MINECRAFT_1_15(573, "1.15"),
MINECRAFT_1_15_1(575, "1.15.1"),
MINECRAFT_1_15_2(578, "1.15.2"),
MINECRAFT_1_16(735, "1.16"),
MINECRAFT_1_16_1(736, "1.16.1"),
MINECRAFT_1_16_2(751, "1.16.2"),
MINECRAFT_1_16_3(753, "1.16.3"),
MINECRAFT_1_16_4(754, "1.16.4"),
MINECRAFT_1_17(755, "1.17"),
MINECRAFT_1_17_1(756, "1.17.1"),
MINECRAFT_1_18(757, "1.18"),
MINECRAFT_1_18_2(758, "1.18.2"),
MINECRAFT_1_19(759, "1.19"),
MINECRAFT_1_19_1(760, "1.19.1"),
MINECRAFT_1_19_3(761, "1.19.3"),
MINECRAFT_1_19_4(762, "1.19.4"),
MINECRAFT_1_20(763, "1.20"),
MINECRAFT_1_20_2(764, "1.20.2"),
MINECRAFT_1_20_3(765, "1.20.3"),
MINECRAFT_1_20_5(766, "1.20.5"),
MINECRAFT_1_21(767, "1.21"),
MINECRAFT_1_21_2(768, "1.21.2"),
MINECRAFT_1_21_4(769, "1.21.4");
private final int protocol;
private final String name;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
static {
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
}
ID_TO_PROTOCOL_CONSTANT = versions;
LATEST_VERSION = values()[values().length - 1];
}
public static ProtocolVersion fromId(final int protocol) {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public boolean greaterThan(final ProtocolVersion that) {
return compareTo(that) > 0;
}
public boolean greaterThanOrEquals(final ProtocolVersion that) {
return compareTo(that) >= 0;
}
public boolean lessThan(final ProtocolVersion that) {
return compareTo(that) < 0;
}
public boolean lessThanOrEquals(final ProtocolVersion that) {
return compareTo(that) <= 0;
}
public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
return compareTo(first) >= 0 && compareTo(last) <= 0;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,12 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command.subcommand.argument;
package xyz.jonesdev.sonar.api.fallback.ratelimit;
public @interface Argument {
import org.jetbrains.annotations.NotNull;
/**
* @return Returns the name of the argument
*/
String value();
@FunctionalInterface
public interface Ratelimiter<T> {
boolean attempt(final @NotNull T t);
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.fingerprint;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
@UtilityClass
public class FingerprintingUtil {
/**
* Returns a hex string representing a hash of the username and IP address
*/
public @NotNull String getFingerprint(final @NotNull String username,
final @NotNull String hostAddress) {
final int hash0 = username.hashCode();
final int hash1 = hostAddress.hashCode();
final int combined = hash0 + hash1;
return Integer.toHexString(hash0 >> 4)
+ Integer.toHexString(combined << 2)
+ Integer.toHexString(hash1 >> 4);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,7 +33,6 @@ public interface LoggerWrapper {
void error(final String message, final Object... args);
// Taken from
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
String ARG_STRING = "{}";
int ARG_STRING_LENGTH = ARG_STRING.length();
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
@@ -23,50 +23,50 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
@Getter
public final class Verbose extends Observable {
public final class ActionBarNotificationHandler extends NotificationHandler {
private int animationIndex;
@Override
public void observe() {
public void handleNotification() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
}
// Prepare the action bar verbose
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final AttackTracker.AttackStatistics attackStatistics = Sonar.get0().getAttackTracker().getCurrentAttack();
final SystemTimer attackTimer = attackStatistics == null ? null : attackStatistics.getDuration();
final String attackDuration = attackTimer == null ? "00:00" : DATE_FORMATTER.format(attackTimer.delay());
final Component actionBarComponent = MiniMessage.miniMessage().deserialize(attackTimer == null
? Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.normal")
: Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.attack"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
? Sonar.get0().getConfig().getMessagesConfig().getString("verbose.layout.normal")
: Sonar.get0().getConfig().getMessagesConfig().getString("verbose.layout.attack"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix()),
Placeholder.unparsed("attack-duration", attackDuration),
Placeholder.unparsed("animation", nextAnimation()),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get0().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get0().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get0().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getLoginsPerSecond())),
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getConnectionsPerSecond())),
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get0().getStatistics().getPerSecondIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get0().getStatistics().getPerSecondOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", formatMemory(Sonar.get0().getStatistics().getTotalIncomingBandwidth())),
Placeholder.unparsed("outgoing-traffic-ttl", formatMemory(Sonar.get0().getStatistics().getTotalOutgoingBandwidth())),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
@@ -74,7 +74,7 @@ public final class Verbose extends Observable {
// Send the action bar to all online players
for (final UUID subscriber : subscribers) {
final Audience audience = Sonar.get().audience(subscriber);
final Audience audience = Sonar.get0().audience(subscriber);
if (audience == null) continue;
// Send the action bar to all online subscribers
@@ -83,7 +83,7 @@ public final class Verbose extends Observable {
}
public String nextAnimation() {
final var animations = Sonar.get().getConfig().getVerboseAnimation();
final var animations = Sonar.get0().getConfig().getVerboseAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
@@ -29,13 +29,13 @@ import java.util.List;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@Getter
public final class Notification extends Observable {
public final class ChatNotificationHandler extends NotificationHandler {
@Override
public void observe() {
public void handleNotification() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
@@ -43,30 +43,30 @@ public final class Notification extends Observable {
// Prepare the chat notification
final Component notificationTitle = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("notifications.title"));
Sonar.get0().getConfig().getMessagesConfig().getString("notifications.title"));
final Component notificationSubtitle = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("notifications.subtitle"));
Sonar.get0().getConfig().getMessagesConfig().getString("notifications.subtitle"));
final Title title = Title.title(notificationTitle, notificationSubtitle);
final List<String> chatNotification = Sonar.get().getConfig().getMessagesConfig().getStringList("notifications.chat");
final List<String> chatNotification = Sonar.get0().getConfig().getMessagesConfig().getStringList("notifications.chat");
final Component[] chatNotificationComponents = new Component[chatNotification.size()];
for (int i = 0; i < chatNotificationComponents.length; i++) {
chatNotificationComponents[i] = MiniMessage.miniMessage().deserialize(chatNotification.get(i),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix()),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get0().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get0().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get0().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getLoginsPerSecond())),
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getConnectionsPerSecond())),
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get0().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get0().getStatistics().getPerSecondIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get0().getStatistics().getPerSecondOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", formatMemory(Sonar.get0().getStatistics().getTotalIncomingBandwidth())),
Placeholder.unparsed("outgoing-traffic-ttl", formatMemory(Sonar.get0().getStatistics().getTotalOutgoingBandwidth())),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
@@ -74,8 +74,8 @@ public final class Notification extends Observable {
}
// Send the title and chat messages to all online players
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
for (final UUID subscriber : Sonar.get0().getChatNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get0().audience(subscriber);
if (audience == null) continue;
// Send the message to all online subscribers
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@@ -25,10 +25,10 @@ import java.util.UUID;
import java.util.Vector;
@Getter
public abstract class Observable {
public abstract class NotificationHandler {
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public abstract void observe();
public abstract void handleNotification();
/**
* @param uuid UUID of the player
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.jvm;
package xyz.jonesdev.sonar.api.profiler;
import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
@@ -24,7 +24,7 @@ import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
@UtilityClass
public class JVMProcessInformation {
public class SimpleProcessProfiler {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.statistics;
@SuppressWarnings("unused")
public interface SonarStatistics {
long getConnectionsPerSecond();
long getLoginsPerSecond();
long getCurrentIncomingBandwidth();
long getCurrentOutgoingBandwidth();
long getTotalIncomingBandwidth();
long getTotalOutgoingBandwidth();
String getPerSecondIncomingBandwidthFormatted();
String getPerSecondOutgoingBandwidthFormatted();
int getTotalPlayersJoined();
int getTotalPlayersVerified();
int getTotalSuccessfulVerifications();
int getTotalFailedVerifications();
long getCurrentAttemptedVerifications();
int getTotalAttemptedVerifications();
long getCurrentBlacklistSize();
long getTotalBlacklistSize();
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.attack;
package xyz.jonesdev.sonar.api.tracker;
import lombok.*;
import org.jetbrains.annotations.Nullable;
@@ -27,12 +27,13 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker {
public static final AttackTracker INSTANCE = new AttackTracker();
private @Nullable AttackStatistics currentAttack;
private int attackThreshold;
@@ -51,10 +52,10 @@ public final class AttackTracker {
}
public void checkIfUnderAttack() {
final long joinsPerSecond = Sonar.get().getStatistics().getLoginsPerSecond();
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getPlayers().size();
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
final long joinsPerSecond = Sonar.get0().getStatistics().getLoginsPerSecond();
final int verifyingPlayers = Sonar.get0().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get0().getFallback().getQueue().getPlayers().size();
final int minPlayers = Sonar.get0().getConfig().getMinPlayersForAttack();
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
|| verifyingPlayers > minPlayers // Check the number of verifying players.
@@ -65,10 +66,10 @@ public final class AttackTracker {
// An attack has been detected
if (currentAttack == null) {
currentAttack = new AttackStatistics();
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().observe();
currentAttack.successfulVerifications = Sonar.get0().getVerifiedPlayerController().getCache().size();
currentAttack.failedVerifications = Sonar.get0().getStatistics().getTotalFailedVerifications();
Sonar.get0().getEventManager().publish(new AttackDetectedEvent());
Sonar.get0().getChatNotificationHandler().handleNotification();
} else {
// Reset attack timer if we're still under attack
currentAttack.timer.reset();
@@ -79,7 +80,7 @@ public final class AttackTracker {
// Update joins per second peak if necessary
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final long connectionsPerSecond = Sonar.get().getStatistics().getConnectionsPerSecond();
final long connectionsPerSecond = Sonar.get0().getStatistics().getConnectionsPerSecond();
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
// Update connections per second peak if necessary
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
@@ -95,14 +96,14 @@ public final class AttackTracker {
currentAttack.peakProcessMemoryUsage = processMemoryUsage;
}
} else if (currentAttack != null) {
if (currentAttack.duration.delay() > Sonar.get().getConfig().getMinAttackDuration()
&& currentAttack.timer.delay() > Sonar.get().getConfig().getAttackCooldownDelay()) {
if (currentAttack.duration.delay() > Sonar.get0().getConfig().getMinAttackDuration()
&& currentAttack.timer.delay() > Sonar.get0().getConfig().getAttackCooldownDelay()) {
// The current attack has stopped
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
Sonar.get0().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
if (++attackThreshold > Sonar.get0().getConfig().getMinAttackThreshold()) {
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
Optional.ofNullable(Sonar.get0().getConfig().getWebhook().getDiscordWebhook())
.ifPresent(webhook -> {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
@@ -115,15 +116,15 @@ public final class AttackTracker {
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
final long blacklisted = Sonar.get0().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long totalVerified = Sonar.get0().getVerifiedPlayerController().getCache().size();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long totalFailed = Sonar.get0().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final SonarConfiguration.Webhook.Embed config = Sonar.get0().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("<start-timestamp>", startTimestamp)
.replace("<end-timestamp>", endTimestamp)
@@ -143,11 +144,9 @@ public final class AttackTracker {
// Reset the attack status
currentAttack = null;
// Reset threshold
attackThreshold = 0;
}
} else {
// Reset threshold
attackThreshold = 0;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,14 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.update;
package xyz.jonesdev.sonar.api.update;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
@@ -36,24 +37,8 @@ import java.util.concurrent.Executors;
@UtilityClass
public class UpdateChecker {
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
private final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[update-checker] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[update-checker] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[update-checker] " + message, args);
}
};
@Getter
private CheckResult lastCheckResult = CheckResult.UNRESOLVED;
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
@@ -63,20 +48,24 @@ public class UpdateChecker {
final JsonObject json = parseJson(urlConnection);
final String latestStableRelease = json.get("tag_name").getAsString();
final int convertedLatestVersion = convertVersion(latestStableRelease);
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getVersion());
final int convertedCurrentVersion = convertVersion(Sonar.get0().getVersion().getVersion());
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().getGitBranch().equals("main")) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.info("You are currently using the latest stable release of Sonar!");
Sonar.get0().getLogger().warn("A new version of Sonar is available: {}", latestStableRelease);
Sonar.get0().getLogger().warn("Please make sure to update to the latest version to ensure stability and security:");
Sonar.get0().getLogger().warn("https://github.com/jonesdevelopment/sonar/releases/latest");
lastCheckResult = CheckResult.OUTDATED_VERSION;
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get0().getVersion().getGitBranch().equals("main")) {
Sonar.get0().getLogger().warn("You are currently using an unreleased version of Sonar!");
Sonar.get0().getLogger().warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
lastCheckResult = CheckResult.UNSTABLE_VERSION;
} else if (lastCheckResult != CheckResult.LATEST_VERSION) {
Sonar.get0().getLogger().info("You are currently using the latest stable release of Sonar!");
lastCheckResult = CheckResult.LATEST_VERSION;
}
} catch (Throwable throwable) {
LOGGER.warn("Unable to retrieve version information: {}", throwable);
Sonar.get0().getLogger().warn("Unable to retrieve version information: {}", throwable);
lastCheckResult = CheckResult.API_ERROR;
}
});
}
@@ -118,4 +107,16 @@ public class UpdateChecker {
final int patch = Integer.parseInt(convertedParts[2]);
return convertedMajor + convertedMinor + patch; // sum the converted version parts
}
@Getter
@RequiredArgsConstructor
public enum CheckResult {
API_ERROR("api-error"),
OUTDATED_VERSION("outdated-version"),
UNSTABLE_VERSION("unstable-version"),
LATEST_VERSION(null),
UNRESOLVED(null);
private final String configKey;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -59,18 +59,18 @@ public final class DiscordWebhook {
// Discord replies with HTTP_NO_CONTENT (204) if the request is successful
final int code = urlConnection.getResponseCode();
if (code != HTTP_NO_CONTENT) {
Sonar.get().getLogger().warn("Unexpected Discord webhook response code {}", code);
Sonar.get0().getLogger().warn("Unexpected Discord webhook response code {}", code);
}
} catch (Exception exception) {
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
Sonar.get0().getLogger().error("Could not send webhook: {}", exception);
}
});
}
private static String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
final String content = Sonar.get().getConfig().getWebhook().getContent();
final String username = Sonar.get().getConfig().getWebhook().getUsername();
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
final String content = Sonar.get0().getConfig().getWebhook().getContent();
final String username = Sonar.get0().getConfig().getWebhook().getUsername();
final String avatarUrl = Sonar.get0().getConfig().getWebhook().getAvatarUrl();
int rgb = embed.getR();
rgb = (rgb << 8) + embed.getG();
@@ -78,8 +78,8 @@ public final class DiscordWebhook {
final Webhook.EmbedMessage embedMessage = new Webhook.EmbedMessage(
embed.getTitle(), embed.getDescription(), embed.getTitleUrl(), rgb, new Webhook.Field[0],
new Webhook.Footer(Sonar.get().getConfig().getWebhook().getFooter().getText(),
Sonar.get().getConfig().getWebhook().getFooter().getIconUrl()));
new Webhook.Footer(Sonar.get0().getConfig().getWebhook().getFooter().getText(),
Sonar.get0().getConfig().getWebhook().getFooter().getIconUrl()));
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
+18 -3
View File
@@ -3,7 +3,8 @@ import net.kyori.indra.git.IndraGitExtension
plugins {
java
alias(libs.plugins.shadow)
alias(libs.plugins.indra.git) apply true
alias(libs.plugins.indra.git)
alias(libs.plugins.spotless)
}
allprojects {
@@ -15,6 +16,7 @@ allprojects {
apply(plugin = "java")
apply(plugin = "com.gradleup.shadow")
apply(plugin = "com.diffplug.spotless")
dependencies {
compileOnly(rootProject.libs.lombok)
@@ -31,6 +33,16 @@ allprojects {
compileOnly(rootProject.libs.libby.core)
}
spotless {
java {
endWithNewline()
formatAnnotations()
removeUnusedImports()
trimTrailingWhitespace()
indentWithSpaces(2)
}
}
tasks {
shadowJar {
// Set the file name of the shadowed jar
@@ -80,14 +92,17 @@ allprojects {
attributes["Git-Commit"] = gitCommit
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
}
}
tasks {
// This is a small wrapper tasks to simplify the building process
register("build-sonar") {
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
val subprojects = listOf("api", "captcha", "common", "bukkit", "bungeecord", "velocity")
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:spotlessApply", "$it:shadowJar") }
dependsOn(buildTasks)
}
}
@@ -46,6 +46,3 @@ tasks {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -120,8 +120,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,10 +27,9 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import java.util.Collections;
import java.util.List;
import static java.util.Collections.emptyList;
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
@Override
@@ -41,11 +40,11 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
Sonar.get().sender(sender),
Sonar.get0().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
return true; // Valid
return true;
}
@Override
@@ -54,9 +53,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
final String commandAlias,
final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
return getCachedTabSuggestions(args);
return sender.hasPermission("sonar.command") ? getCachedTabSuggestions(args) : Collections.emptyList();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -70,7 +70,11 @@ public enum BukkitServerVersion {
MINECRAFT_1_20_4,
MINECRAFT_1_20_5,
MINECRAFT_1_20_6,
MINECRAFT_1_21;
MINECRAFT_1_21,
MINECRAFT_1_21_1,
MINECRAFT_1_21_2,
MINECRAFT_1_21_3,
MINECRAFT_1_21_4;
static final BukkitServerVersion[] REVERSED_VALUES;
@@ -1,3 +1,20 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBufAllocator;
@@ -7,6 +24,7 @@ import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
import java.util.HashSet;
@@ -16,7 +34,7 @@ public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
for (final ChannelInboundHandler handler : handlers) {
try {
@@ -28,9 +46,9 @@ public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter
super.channelInactive(ctx);
}
public void add(ChannelHandler handler) {
if (handler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) handler);
public void add(final @NotNull ChannelHandler channelHandler) {
if (channelHandler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) channelHandler);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
@@ -28,23 +28,25 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetSocketAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
private FallbackPacketRegistry.ProtocolRegistry registry;
FallbackBukkitInboundHandler() {
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
channelRemovalListener = (pipeline, name, handler) -> {
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
final ChannelInactiveListener inactiveListener = pipeline.get(ChannelInactiveListener.class);
if (inactiveListener != null) {
inactiveListener.add(handler);
@@ -52,22 +54,21 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
};
}
private FallbackPacketRegistry.ProtocolRegistry registry;
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
public void updateRegistry(final @NotNull FallbackPacketRegistry registry,
final @NotNull ProtocolVersion protocolVersion) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
this.registry = registry.getProtocolRegistry(FallbackPacketRegistry.Direction.SERVERBOUND, protocolVersion);
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Reset the reader index, so we can read the content
// Make sure to store the original reader index
/*
* Reset the reader index, so we can read the content;
* We also have to make sure to store the original reader index.
* We do this to 'spoof' the packet state - we want to read it without the server knowing.
*/
final int originalReaderIndex = byteBuf.readerIndex();
byteBuf.readerIndex(0);
@@ -77,12 +78,11 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
return;
}
final int packetId = readVarInt(byteBuf);
final int packetId = ProtocolUtil.readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
@@ -91,53 +91,55 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
} catch (Throwable throwable) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Failed to decode packet");
throw ProtocolUtil.DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Could not read packet to end");
throw ProtocolUtil.DEBUG ? new DecoderException("Could not read packet to end ("
+ byteBuf.readableBytes() + " bytes left)") : QuietDecoderException.INSTANCE;
}
// Useful resources:
// https://wiki.vg/Protocol#Handshaking
// https://wiki.vg/Protocol#Login
/*
* Useful resources:
* https://wiki.vg/Protocol#Handshaking
* https://wiki.vg/Protocol#Login
*/
if (packet instanceof HandshakePacket) {
final HandshakePacket handshake = (HandshakePacket) packet;
switch (handshake.getIntent()) {
case STATUS:
// We don't care about server pings or transfers; remove the handler
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
break;
case LOGIN:
case TRANSFER:
// Let the actual handler know about the handshake packet
handleHandshake(ctx.channel(), handshake.getHostname(), handshake.getProtocolVersionId());
handleHandshake(ctx, handshake.getHostname(), handshake.getProtocolVersionId());
// Be ready for the next packet (which is supposed to be a login packet)
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
break;
default:
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
throw ProtocolUtil.DEBUG ? new DecoderException("Bad handshake intent " + handshake.getIntent())
: QuietDecoderException.INSTANCE;
}
} else if (packet instanceof LoginStartPacket) {
final LoginStartPacket loginStart = (LoginStartPacket) packet;
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> {
handleLogin(ctx, () -> {
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf.retain());
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
// TODO: recode this?
final ChannelHandler inboundHandler = ctx.pipeline().remove(FALLBACK_INBOUND_HANDLER);
if (inboundHandler != null) {
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}, loginStart.getUsername(), socketAddress);
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,18 +25,18 @@ import lombok.Getter;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import xyz.jonesdev.sonar.common.util.exception.ReflectiveOperationException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.bukkit.SonarBukkit.INITIALIZE_LISTENER;
// Check out these links if you want to see some more magic
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
@@ -196,8 +196,7 @@ public class FallbackBukkitInjector {
for (int i = 0; i < 2; i++) {
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
// Check if the field has the correct generic type;
// We need this field to be List<ChannelFuture>
// Check if the field has the correct generic type; We need this field to be List<ChannelFuture>
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
continue;
}
@@ -235,27 +234,29 @@ public class FallbackBukkitInjector {
childHandlerField.setAccessible(true);
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
ChannelHandler finalBootstrap = bootstrap;
/*
* Inject our own channel initializer into the original field
* If the plugin is not fully enabled but executes the injection,
* put an initializer to close the new connection automatically.
*/
if (!INITIALIZE_LISTENER.isDone()) {
childHandlerField.set(bootstrap, new ChannelInitializer<>() {
// Inject our own channel initializer into the original field
// If the plugin is not fully enabled but executes the injection,
// put an initializer to close the new connection automatically.
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
protected void initChannel(final @NotNull Channel channel) {
channel.close();
}
};
childHandlerField.set(finalBootstrap, initializer);
});
}
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
final ChannelHandler _bootstrap = bootstrap;
INITIALIZE_LISTENER.thenAccept(__ -> {
try {
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
childHandlerField.set(_bootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
pipeline -> {
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
pipeline.addFirst(FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
}
));
} catch (IllegalAccessException exception) {
@@ -266,7 +267,7 @@ public class FallbackBukkitInjector {
return;
}
} catch (Exception exception) {
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
Sonar.get0().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -34,7 +34,6 @@ import java.lang.reflect.Method;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
// Taken from
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
public final class BukkitJoinListener implements Listener {
private static MethodHandle _handleMethod;
@@ -84,7 +83,7 @@ public final class BukkitJoinListener implements Listener {
}
// Make sure the player is not a fake player
if (FakeChannelUtil.isFakePlayer(channel)) {
if (FakeChannelUtil.isFakeChannel(channel)) {
return;
}
@@ -93,7 +92,7 @@ public final class BukkitJoinListener implements Listener {
channel.close();
}
} catch (Throwable throwable) {
Sonar.get().getLogger().warn("Couldn't find {}: {}", event.getPlayer().getName(), throwable);
Sonar.get0().getLogger().warn("Couldn't find {}: {}", event.getPlayer().getName(), throwable);
}
}
}
@@ -26,8 +26,7 @@ dependencies {
tasks {
shadowJar {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
archiveFileName = "Sonar-Bungee.jar" // Hardcode a shortened version of the name
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -86,6 +86,10 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void enable() {
// Add warning because of questionable design choices that haunt me in my dreams.
logger.warn("The use of BungeeCord is supported but not condoned.");
logger.warn("Consider upgrading to Velocity (https://velocitypowered.com)");
// Initialize bStats.org metrics
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
@@ -108,8 +112,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,7 +26,7 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import static java.util.Collections.emptyList;
import java.util.Collections;
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
@@ -38,7 +38,7 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
Sonar.get().sender(sender),
Sonar.get0().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -47,9 +47,6 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
@Override
public Iterable<String> onTabComplete(final @NotNull CommandSender sender, final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
return getCachedTabSuggestions(args);
return sender.hasPermission("sonar.command") ? getCachedTabSuggestions(args) : Collections.emptyList();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
@@ -26,13 +25,15 @@ import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import xyz.jonesdev.sonar.common.util.exception.ReflectiveOperationException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.net.InetSocketAddress;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.STATUS;
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
@@ -47,46 +48,45 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
// Skip any unknown or invalid packets
if (wrappedPacket != null) {
final Channel channel = ctx.channel();
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
// We don't care about server pings; remove the handler
if (handshake.getRequestedProtocol() == 1) {
ctx.channel().pipeline().remove(this);
if (handshake.getRequestedProtocol() == STATUS) {
ctx.pipeline().remove(this);
} else {
handleHandshake(ctx, handshake.getHost(), handshake.getProtocolVersion());
}
}
// Intercept any server login packet by the client
else if (wrappedPacket instanceof LoginRequest) {
} else if (wrappedPacket instanceof LoginRequest) {
// Make sure to use the potentially modified, real IP
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
final ChannelWrapper channelWrapper;
try {
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
} catch (final Throwable throwable) {
throw new ReflectiveOperationException(throwable);
final HandlerBoss handlerBoss = ctx.pipeline().get(HandlerBoss.class);
// This might sometimes happen if the client unexpectedly disconnects
if (handlerBoss != null) {
// Get the real IP address of the player through the channel wrapper
final ChannelWrapper channelWrapper;
try {
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
} catch (final Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
// We've done our job - deject this pipeline
ctx.pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
handleLogin(ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
return;
}
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
handleLogin(channel, ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
return;
}
}
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import lombok.val;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.channel.BungeeChannelInitializer;
import org.jetbrains.annotations.NotNull;
import sun.misc.Unsafe;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
@UtilityClass
public class FallbackBungeeInjector {
public void inject() {
try {
final Field childField;
try {
// This field only exists on BungeeCord build 1908 and below.
// See https://github.com/SpigotMC/BungeeCord/pull/3776 for more info.
//noinspection all
childField = PipelineUtils.class.getField("SERVER_CHILD");
} catch (NoSuchFieldException exception) {
injectOnModernBungeeCord();
return;
}
childField.setAccessible(true);
injectOnOldBungeeCord(childField);
} catch (Exception exception) {
Sonar.get0().getLogger().error("An error occurred while injecting {}", exception);
}
}
private static void injectOnOldBungeeCord(final @NotNull Field childField) throws Exception {
// Make sure to store the original channel initializer
//noinspection unchecked
val originalInitializer = (ChannelInitializer<Channel>) childField.get(null);
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackBungeeInboundHandler()));
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
final Object base = unsafe.staticFieldBase(childField);
final long offset = unsafe.staticFieldOffset(childField);
// Replace the original channel initializer with our new injected initializer
unsafe.putObject(base, offset, injectedInitializer);
}
// See https://github.com/SpigotMC/BungeeCord/pull/3787
private static void injectOnModernBungeeCord() {
// The order is important...
final BungeeChannelInitializer original = ProxyServer.getInstance().unsafe().getFrontendChannelInitializer();
final BungeeChannelInitializer newInitializer = BungeeChannelInitializer.create(
channel -> {
// https://github.com/SpigotMC/BungeeCord/pull/3787#issuecomment-2661059876
if (!original.getChannelAcceptor().accept(channel)) {
return false;
}
FallbackInjectedChannelInitializer.inject(channel,
pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackBungeeInboundHandler()));
return true;
});
ProxyServer.getInstance().unsafe().setFrontendChannelInitializer(newInitializer);
}
}
@@ -5,6 +5,3 @@ dependencies {
testCompileOnly(project(":api"))
testCompileOnly(rootProject.libs.imagefilters)
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,15 +33,11 @@ import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Random;
import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.FONTS;
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.STANDARD_FONT_SIZE;
@Getter
@RequiredArgsConstructor
public final class StandardCaptchaGenerator implements CaptchaGenerator {
@@ -69,12 +65,12 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
}
}
private final int width, height;
private final @Nullable InputStream background;
private BufferedImage backgroundImage;
private final int width = 128, height = 128;
private final @Nullable File background;
private @Nullable BufferedImage backgroundImage;
@Override
public @NotNull BufferedImage createImage(final char[] answer) {
public @NotNull BufferedImage createImage(final char @NotNull [] answer) {
final BufferedImage image = createBackgroundImage();
final Graphics2D graphics = image.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
@@ -91,16 +87,22 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
}
private @NotNull BufferedImage createBackgroundImage() {
// Create a new image buffer with a 3-byte color spectrum
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
if (background == null) {
final BufferedImage image = new BufferedImage(width, height, TYPE_3BYTE_BGR);
// Fill the entire image with a noise texture
return FBM.filter(image, image);
}
try {
return backgroundImage == null ? backgroundImage = ImageIO.read(background) : backgroundImage;
} catch (IOException exception) {
throw new IllegalStateException("Could not read background image", exception);
// Try to load the image from a file if it doesn't exist yet
if (backgroundImage == null) {
try {
backgroundImage = ImageIO.read(background);
} catch (IOException exception) {
throw new IllegalStateException("Could not read background image", exception);
}
}
image.createGraphics().drawImage(backgroundImage, 0, 0, Color.WHITE, null);
return image;
}
private void applyRandomColorGradient(final @NotNull Graphics2D graphics) {
@@ -122,7 +124,7 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
for (int i = 0; i < answer.length; i++) {
// Create a glyph vector for the character with a random font
final Font font = FONTS[RANDOM.nextInt(FONTS.length)];
final Font font = StandardTTFFontProvider.FONTS[RANDOM.nextInt(StandardTTFFontProvider.FONTS.length)];
glyphs[i] = font.createGlyphVector(ctx, String.valueOf(answer[i]));
}
@@ -133,7 +135,7 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
.mapToDouble(glyph -> glyph.getLogicalBounds().getWidth() * scalingXY - 1)
.sum();
double beginX = Math.max(Math.min(width / 2D - totalWidth / 2D, totalWidth), 0);
double beginY = (height + STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
double beginY = (height + StandardTTFFontProvider.STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
// Draw each glyph one by one
for (final GlyphVector glyph : glyphs) {
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,7 +28,7 @@ import java.util.Random;
public class Main {
public void main(final String... args) throws IOException {
final Random random = new Random();
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(128, 128, null);
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(null);
final long start = System.currentTimeMillis();
final char[] dictionary = {'a', 'b', 'c', 'd', 'e', 'f'/*, 'g'*/, 'h'/*, 'i'*/, 'j',
@@ -3,6 +3,3 @@ dependencies {
implementation(project(":captcha"))
compileOnly(rootProject.libs.adventure.nbt)
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -47,7 +47,7 @@ class LibraryLoader {
Library.builder()
.groupId("com{}github{}ben-manes{}caffeine")
.artifactId("caffeine")
.version("3.1.8")
.version("3.2.0")
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
.build(),
// ORMLite
@@ -72,19 +72,19 @@ class LibraryLoader {
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-minimessage")
.version("4.17.0")
.version("4.13.1") // adventure-platforms hasn't updated in years... Please update! :'(
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-serializer-gson")
.version("4.17.0")
.version("4.18.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-nbt")
.version("4.17.0")
.version("4.18.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build()
);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -31,17 +31,17 @@ import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
import xyz.jonesdev.sonar.common.fallback.ratelimit.CaffeineCacheRatelimiter;
import xyz.jonesdev.sonar.common.fallback.ratelimit.NoopCacheRatelimiter;
import xyz.jonesdev.sonar.common.service.ScheduledServiceManager;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommands.*;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import xyz.jonesdev.sonar.common.subcommand.*;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.io.File;
@@ -52,9 +52,9 @@ import java.time.Duration;
public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
private ActionBarNotificationHandler actionBarNotificationHandler;
@Setter
private Notification notificationHandler;
private ChatNotificationHandler chatNotificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private final LibraryManager libraryManager;
@@ -79,8 +79,8 @@ public abstract class SonarBootstrap<T> implements Sonar {
this.platform = platform;
// Load the rest of the components
this.statistics = new GlobalSonarStatistics();
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.actionBarNotificationHandler = new ActionBarNotificationHandler();
this.chatNotificationHandler = new ChatNotificationHandler();
this.config = new SonarConfiguration(dataDirectory);
// Register all subcommands
this.subcommandRegistry = new SubcommandRegistry();
@@ -97,13 +97,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
public final void initialize() {
// Check if the branch is not the main branch to warn about unstable versions
if (!getVersion().getGitBranch().equals("main")) {
getLogger().warn("You are currently using an unofficial experimental branch.");
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
}
getLogger().info("Successfully booted in {}s!", launchTimer);
getLogger().info("Initializing shared components...");
@@ -119,7 +112,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Start threads
getLogger().info("Starting all managed threads...");
SonarServiceManager.start();
ScheduledServiceManager.start();
// Done
getLogger().info("Done ({}s)!", launchTimer);
@@ -127,12 +120,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
// An error has occurred
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
throwable.printStackTrace(System.err);
return; // Do not check for updates if the launch failed
}
// Check if a new version has been released if enabled in the configuration
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
UpdateChecker.checkForUpdates();
}
}
@@ -148,30 +135,14 @@ public abstract class SonarBootstrap<T> implements Sonar {
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Update the CAPTCHA generator if necessary
if (getConfig().getVerification().getMap().getTiming() == SonarConfiguration.Verification.Timing.NEVER) {
getFallback().setCaptchaGenerator(null);
} else if (getFallback().getCaptchaGenerator() == null
|| getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare cached packets
getLogger().info("Taking cached snapshots of all packets...");
FallbackPreparer.prepare();
// Update ratelimiter caches
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.ticker(Ticker.systemTicker())
.build());
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
.ticker(Ticker.systemTicker())
.build());
// Update ratelimiter cache
getFallback().setRatelimiter(getConfig().getVerification().getReconnectDelay() > 0L
? new CaffeineCacheRatelimiter(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
: NoopCacheRatelimiter.INSTANCE);
// Update blacklist cache
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
@@ -204,7 +175,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Initialize the shutdown process
getLogger().info("Starting shutdown process...");
// Interrupt threads
SonarServiceManager.stop();
ScheduledServiceManager.stop();
// Close database connection if present
if (verifiedPlayerController != null) {
verifiedPlayerController.close();
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,9 +25,7 @@ import io.netty.channel.ChannelPromise;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -35,27 +33,22 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
public static final ChannelHandler INSTANCE = new FallbackBandwidthHandler();
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
// We can only get the size of a message if it's a ByteBuf
public void channelRead(final ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// Increment the incoming traffic by the number of readable bytes
INCOMING.increment(((ByteBuf) msg).readableBytes());
final int readableBytes = ((ByteBuf) msg).readableBytes();
GlobalSonarStatistics.perSecondIncomingTraffic += readableBytes;
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
@Override
public void write(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg,
final ChannelPromise promise) throws Exception {
// We can only get the size of a message if it's a ByteBuf
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf) {
// Increment the outgoing traffic by the number of readable bytes
OUTGOING.increment(((ByteBuf) msg).readableBytes());
final int readableBytes = ((ByteBuf) msg).readableBytes();
GlobalSonarStatistics.perSecondOutgoingTraffic += readableBytes;
}
// Make sure to let the server handle the rest
ctx.write(msg, promise);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -41,7 +41,7 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
// Make sure to let the server handle this
ctx.fireChannelActive();
// Add the packet handler pipeline
sonarPipelineInjector.accept(ctx.pipeline());
@@ -49,26 +49,41 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Make sure to let the server handle this
ctx.fireChannelInactive();
// The player can disconnect without sending the login packet first
// Account for this by checking if the inetAddress has been set yet
if (inetAddress != null) {
// Remove the IP address from the connected players
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
// Remove the IP address from the queue
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
// Remove this account from the online players or decrement the number of accounts with the same IP
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
Sonar.get0().getFallback().getQueue().getPlayers().compute(inetAddress, (ignored, action) -> {
// The player is not queued, so we need to remove them from other maps as well
if (action == null) {
// Remove the IP address from the connected players, if needed
Sonar.get0().getFallback().getConnected().compute(inetAddress, (__, v) -> {
/*
* Remove this account from the online players or decrement the number of accounts
* with the same IP, but only if the player is logging into the backend server.
* We don't need to decrement the count if the player was just verified since
* we've never actually incremented it in the first place ¯\_(ツ)_/¯
*/
if (v == null) {
Sonar.get0().getFallback().getOnline().compute(inetAddress,
(ignored1, count) -> count == null || count <= 1 ? null : count - 1);
}
return null;
});
}
return null;
});
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
/*
* We can override the default exceptionCaught method since this handler
* will run before the connection knows that there has been an error.
*/
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable cause) throws Exception {
ctx.close();
}
}
@@ -0,0 +1,262 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.*;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserUtil;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@AllArgsConstructor
@RequiredArgsConstructor
public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
protected @Nullable String username;
protected ProtocolVersion protocolVersion;
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
/**
* Validates and handles incoming handshake packets
*/
protected final void handleHandshake(final @NotNull ChannelHandlerContext ctx,
final @NotNull String hostname,
final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw QuietDecoderException.INSTANCE;
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw QuietDecoderException.INSTANCE;
}
protocolVersion = ProtocolVersion.fromId(protocol);
ctx.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*/
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Runnable initialLoginAction,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress) throws Exception {
// Count every single attempted login
GlobalSonarStatistics.countLogin();
// Ensure that the player sent a handshake packet
if (protocolVersion == null) {
throw QuietDecoderException.INSTANCE;
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections.
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
if (protocolVersion.isUnknown()) {
customDisconnect(ctx.channel(), unsupportedVersion, ProtocolVersion.MINECRAFT_1_7_2);
return;
}
// Ensure that the player has not sent a login packet yet
if (this.username != null) {
throw QuietDecoderException.INSTANCE;
}
this.username = username;
final InetAddress inetAddress = socketAddress.getAddress();
ctx.pipeline().get(FallbackInboundHandler.class).setInetAddress(inetAddress);
// Check if Fallback is already verifying a player with the same IP address
if (Sonar.get0().getFallback().getConnected().containsKey(inetAddress)) {
customDisconnect(ctx.channel(), alreadyVerifying, protocolVersion);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get0().getConfig().getVerification().getBlacklistedProtocols().contains(protocolVersion.getProtocol())) {
customDisconnect(ctx.channel(), protocolBlacklisted, protocolVersion);
return;
}
final String hostAddress = inetAddress.getHostAddress();
// Check if the player failed the verification too many times
final int limit = Sonar.get0().getConfig().getVerification().getBlacklistThreshold();
if (limit > 0) {
final int score = Sonar.get0().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
if (score >= limit) {
customDisconnect(ctx.channel(), blacklisted, protocolVersion);
return;
}
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get0().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx.channel(), inetAddress, initialLoginAction);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserUtil.isGeyserConnection(ctx.channel(), socketAddress);
if (geyser && !Sonar.get0().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx.channel(), inetAddress, initialLoginAction);
return;
}
// Make sure we actually have to verify the player
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
if (Sonar.get0().getVerifiedPlayerController().getCache().contains(fingerprint)) {
initialLogin(ctx.channel(), inetAddress, initialLoginAction);
return;
}
// Check if the IP address is currently being rate-limited
if (!Sonar.get0().getFallback().getRatelimiter().attempt(inetAddress)) {
customDisconnect(ctx.channel(), reconnectedTooFast, protocolVersion);
return;
}
// Remove all other pipelines that could still mess up something
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
Sonar.get0().getFallback().getQueue().getPlayers().compute(inetAddress, (__, runnable) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
customDisconnect(ctx.channel(), alreadyQueued, protocolVersion);
// Remove other instances of this IP address from the queue
return null;
}
// Create an instance for the user and let the verification handler take over the channel
return () -> new FallbackUserWrapper(ctx, inetAddress, protocolVersion, username, fingerprint, geyser);
});
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*/
protected final void initialLogin(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull Runnable loginPacket) throws Exception {
Sonar.get0().getFallback().getOnline().compute(inetAddress, (__, count) -> {
final int maxOnlinePerIp = Sonar.get0().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (count != null && maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (count >= maxOnlinePerIp) {
customDisconnect(channel, tooManyOnlinePerIP, protocolVersion);
return count + 1;
}
}
// Let the server know about the login packet
loginPacket.run();
// Increment the number of accounts with the same IP
return count == null ? 1 : count + 1;
});
}
/**
* Removes all pipelines and rewrites them using our custom handlers
*/
private static void rewriteProtocol(final @NotNull ChannelHandlerContext ctx,
final @NotNull RemovalListener removalListener) {
for (final Map.Entry<String, ChannelHandler> entry : ctx.pipeline()) {
// Don't accidentally remove Sonar's handlers
if (entry.getKey().startsWith("sonar")
// Don't remove floodgate's pipelines
|| entry.getKey().startsWith("geyser")
|| entry.getKey().startsWith("floodgate")
|| entry.getKey().equals("DefaultChannelPipeline$TailContext#0")) {
continue;
}
ctx.pipeline().remove(entry.getValue());
removalListener.accept(ctx.pipeline(), entry.getKey(), entry.getValue());
}
// Add our read/write timeout handler
ctx.pipeline().addFirst(FALLBACK_TIMEOUT, new FallbackTimeoutHandler(
Sonar.get0().getConfig().getVerification().getReadTimeout(),
Sonar.get0().getConfig().getVerification().getWriteTimeout(),
TimeUnit.MILLISECONDS));
}
private static void customDisconnect(final @NotNull Channel channel,
final @NotNull FallbackPacket packet,
final @NotNull ProtocolVersion protocolVersion) {
if (channel.isActive()) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, packet, protocolVersion);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, packet, protocolVersion));
}
}
}
/**
* Disconnect the player before verification (during login)
*/
private static void _customDisconnect(final @NotNull Channel channel,
final @NotNull FallbackPacket packet,
final @NotNull ProtocolVersion protocolVersion) {
// TODO: recode this
// Remove the connection handler pipeline to completely take over the channel
final String handler = Sonar.get0().getPlatform().getConnectionHandler();
if (channel.pipeline().context(handler) != null) {
channel.pipeline().remove(handler);
}
final String encoder = Sonar.get0().getPlatform().getEncoder().apply(channel.pipeline());
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no encoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it has already been replaced by Sonar
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
newEncoder.updateRegistry(FallbackPacketRegistry.LOGIN);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
ProtocolUtil.closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
@FunctionalInterface
public interface RemovalListener {
void accept(final @NotNull ChannelPipeline pipeline,
final @NotNull String name,
final @NotNull ChannelHandler handler);
RemovalListener EMPTY = (pipeline, name, handler) -> {};
}
}
@@ -0,0 +1,138 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarIntLengthEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
import xyz.jonesdev.sonar.common.fallback.verification.FallbackPreJoinHandler;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.net.InetAddress;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
@Getter
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
public final class FallbackUserWrapper implements FallbackUser {
@Accessors(fluent = true)
private final Channel channel;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
private final String fingerprint;
private final String username;
private final boolean geyser;
private final SystemTimer loginTimer;
@Setter
private boolean forceCaptcha;
public FallbackUserWrapper(final @NotNull ChannelHandlerContext ctx,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull String username,
final @NotNull String fingerprint,
final boolean geyser) {
this.channel = ctx.channel();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.username = username;
this.fingerprint = fingerprint;
this.geyser = geyser;
this.loginTimer = new SystemTimer();
GlobalSonarStatistics.totalAttemptedVerifications++;
if (Sonar.get0().getConfig().getVerification().isLogConnections()
&& (Sonar.get0().getAttackTracker().getCurrentAttack() == null
|| Sonar.get0().getConfig().getVerification().isLogDuringAttack())) {
Sonar.get0().getLogger().info(
Sonar.get0().getConfig().getMessagesConfig().getString("verification.logs.connection")
.replace("<username>", username)
.replace("<ip>", Sonar.get0().getConfig().formatAddress(inetAddress))
.replace("<protocol>", protocolVersion.getName()));
}
// Call the VerifyJoinEvent for external API usage
Sonar.get0().getEventManager().publish(new UserVerifyJoinEvent(this));
// Run this in the channel's event loop to avoid issues
channel.eventLoop().execute(() -> {
// Make sure the channel is still active
if (!channel.isActive()) {
return;
}
// Mark the player as connected by caching them in a map of verifying players
Sonar.get0().getFallback().getConnected().compute(inetAddress, (__, v) -> true);
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().addFirst(FALLBACK_FRAME_ENCODER, FallbackVarIntLengthEncoder.INSTANCE);
channel.pipeline().addLast(FALLBACK_PACKET_ENCODER, newEncoder);
// Replace normal decoder to allow custom packets
final FallbackPacketDecoder newDecoder = new FallbackPacketDecoder(protocolVersion);
channel.pipeline().addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
channel.pipeline().addLast(FALLBACK_PACKET_DECODER, newDecoder);
// We're sending the LoginSuccess packet now
newDecoder.updateRegistry(FallbackPacketRegistry.LOGIN);
newEncoder.updateRegistry(FallbackPacketRegistry.LOGIN);
// Send LoginSuccess packet to make the client think they are joining the server
write(FallbackPreparer.loginSuccess);
// pre-1.20.2 clients do not have the configuration stage
if (protocolVersion.lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
newDecoder.updateRegistry(FallbackPacketRegistry.GAME);
newEncoder.updateRegistry(FallbackPacketRegistry.GAME);
}
// Listen for all incoming packets by setting the packet listener
newDecoder.setListener(new FallbackPreJoinHandler(this));
// Make sure to catch all exceptions during the verification
channel.pipeline().addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
});
}
@Override
public void disconnect(final @NotNull Component reason) {
final FallbackPacketEncoder encoder = channel.pipeline().get(FallbackPacketEncoder.class);
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() == FallbackPacketRegistry.LOGIN;
ProtocolUtil.closeWith(channel, protocolVersion, new DisconnectPacket(reason, duringLogin));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,16 +18,15 @@
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
import xyz.jonesdev.sonar.common.util.exception.ReflectiveOperationException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -61,13 +60,17 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
throw new ReflectiveOperationException(throwable);
}
inject(channel, sonarPipelineInjector);
}
public static void inject(final @NotNull Channel channel, final Consumer<ChannelPipeline> sonarPipelineInjector) {
// Inject Sonar's channel handler into the pipeline;
// Also make sure the player is not a fake player to avoid compatibility issues
if (channel.isActive() && !FakeChannelUtil.isFakePlayer(channel)) {
final ChannelHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
if (channel.isActive() && !FakeChannelUtil.isFakeChannel(channel)) {
final FallbackInboundHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
// We need to be careful on Bukkit, as the encoder can be different
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
if (Sonar.get0().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get0().getPlatform().getEncoder().apply(channel.pipeline());
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
} else {
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,8 +30,7 @@ public final class FallbackTailExceptionsHandler extends ChannelDuplexHandler {
// does not have any other pipelines that could handle this error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleStateEvent;
@@ -26,12 +26,12 @@ import java.util.concurrent.TimeUnit;
public final class FallbackTimeoutHandler extends IdleStateHandler {
public FallbackTimeoutHandler(final int readTimeout, final int writeTimeout, final TimeUnit timeUnit) {
super(readTimeout, writeTimeout, 0L, timeUnit);
super(false, readTimeout, writeTimeout, 0L, timeUnit);
}
@Override
protected void channelIdle(final @NotNull ChannelHandlerContext ctx,
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
final IdleStateEvent idleStateEvent) throws Exception {
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
// still throws the Exception even if the channel is closed
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.DecoderException;
import io.netty.util.ByteProcessor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.List;
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf byteBuf,
final @NotNull List<Object> out) throws Exception {
if (!ctx.channel().isActive()) {
byteBuf.clear();
return;
}
// Skip any runs of 0x00 we might find
final int packetStart = byteBuf.forEachByte(ByteProcessor.FIND_NON_NUL);
if (packetStart == -1) {
byteBuf.clear();
return;
}
byteBuf.readerIndex(packetStart);
// Try to read the length of the packet
byteBuf.markReaderIndex();
final int preIndex = byteBuf.readerIndex();
final int length = readRawVarInt21(byteBuf);
if (preIndex == byteBuf.readerIndex()) {
return;
}
if (length <= 0) {
throw ProtocolUtil.DEBUG ? new DecoderException("Bad VarInt length: " + length) : QuietDecoderException.INSTANCE;
}
if (byteBuf.readableBytes() < length) {
byteBuf.resetReaderIndex();
} else {
out.add(byteBuf.readRetainedSlice(length));
}
}
private static int readRawVarInt21(final @NotNull ByteBuf byteBuf) {
if (byteBuf.readableBytes() < 4) {
return readRawVarIntSmallBuffer(byteBuf);
}
// take the last three bytes and check if any of them have the high bit set
final int wholeOrMore = byteBuf.getIntLE(byteBuf.readerIndex());
final int atStop = ~wholeOrMore & 0x808080;
if (atStop == 0) {
throw ProtocolUtil.DEBUG ? new DecoderException("VarInt too big") : QuietDecoderException.INSTANCE;
}
final int bitsToKeep = Integer.numberOfTrailingZeros(atStop) + 1;
byteBuf.skipBytes(bitsToKeep >> 3);
// https://github.com/netty/netty/pull/14050#issuecomment-2107750734
int preservedBytes = wholeOrMore & (atStop ^ (atStop - 1));
// https://github.com/netty/netty/pull/14050#discussion_r1597896639
preservedBytes = (preservedBytes & 0x007F007F) | ((preservedBytes & 0x00007F00) >> 1);
preservedBytes = (preservedBytes & 0x00003FFF) | ((preservedBytes & 0x3FFF0000) >> 2);
return preservedBytes;
}
private static int readRawVarIntSmallBuffer(final @NotNull ByteBuf byteBuf) {
if (!byteBuf.isReadable()) {
return 0;
}
byteBuf.markReaderIndex();
byte tmp = byteBuf.readByte();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7F;
if (!byteBuf.isReadable()) {
byteBuf.resetReaderIndex();
return 0;
}
if ((tmp = byteBuf.readByte()) >= 0) {
return result | tmp << 7;
}
result |= (tmp & 0x7F) << 7;
if (!byteBuf.isReadable()) {
byteBuf.resetReaderIndex();
return 0;
}
if ((tmp = byteBuf.readByte()) >= 0) {
return result | tmp << 14;
}
return result | (tmp & 0x7F) << 14;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,31 +20,28 @@ package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.varIntBytes;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
import java.util.List;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
@ChannelHandler.Sharable
public final class FallbackVarInt21FrameEncoder extends MessageToByteEncoder<ByteBuf> {
public static final FallbackVarInt21FrameEncoder INSTANCE = new FallbackVarInt21FrameEncoder();
public final class FallbackVarIntLengthEncoder extends MessageToMessageEncoder<ByteBuf> {
public static final FallbackVarIntLengthEncoder INSTANCE = new FallbackVarIntLengthEncoder();
@Override
protected void encode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final @NotNull ByteBuf out) throws Exception {
writeVarInt(out, msg.readableBytes());
out.writeBytes(msg);
}
final @NotNull ByteBuf byteBuf,
final @NotNull List<Object> out) throws Exception {
final int readableBytes = byteBuf.readableBytes();
final int length = ProtocolUtil.varIntBytes(readableBytes);
@Override
protected ByteBuf allocateBuffer(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
final int anticipatedRequiredCapacity = varIntBytes(msg.readableBytes()) + msg.readableBytes();
return ctx.alloc().buffer(anticipatedRequiredCapacity);
final ByteBuf lenBuf = ctx.alloc().buffer(length);
ProtocolUtil.writeVarInt(lenBuf, readableBytes);
out.add(lenBuf);
out.add(byteBuf.retain());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,11 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.CaptchaGenerationEndEvent;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapCaptchaInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapColorPalette;
import java.awt.image.BufferedImage;
import java.util.Random;
@@ -41,12 +44,12 @@ public class CaptchaPreparer {
}
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
Sonar.get0().getLogger().info("Asynchronously preparing CAPTCHA answers...");
Sonar.get0().getLogger().info("Players will be able to join even if the preparation isn't finished");
// Prepare cache
final int precomputeAmount = Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount();
final char[] alphabet = Sonar.get().getConfig().getVerification().getMap().getAlphabet().toCharArray();
final int precomputeAmount = Sonar.get0().getConfig().getVerification().getMap().getPrecomputeAmount();
final char[] alphabet = Sonar.get0().getConfig().getVerification().getMap().getAlphabet().toCharArray();
preparedAmount = 0;
cached = new MapCaptchaInfo[precomputeAmount];
@@ -54,18 +57,18 @@ public class CaptchaPreparer {
// Prepare everything asynchronously
PREPARATION_SERVICE.execute(() -> {
for (preparedAmount = 0; preparedAmount < precomputeAmount;) {
// Generate CAPTCHA answer
// Generate CAPTCHA
final char[] answer = new char[3 + RANDOM.nextInt(2)];
for (int j = 0; j < answer.length; j++) {
answer[j] = alphabet[RANDOM.nextInt(alphabet.length)];
}
final BufferedImage image = Sonar.get().getFallback().getCaptchaGenerator().createImage(answer);
final BufferedImage image = Sonar.get0().getFallback().getCaptchaGenerator().createImage(answer);
// Convert and cache converted Minecraft map bytes
final int[] buffer = MapColorPalette.getBufferFromImage(image);
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), buffer);
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), MapColorPalette.imageToBuffer(image));
}
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
Sonar.get0().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
Sonar.get0().getEventManager().publish(new CaptchaGenerationEndEvent(timer, preparedAmount));
});
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -39,20 +39,18 @@ public interface FallbackPacket {
void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception;
/**
* @param byteBuf ByteBuf
* @param protocolVersion Protocol version of the player
* @return The minimum allowed length of the decoded packet
*/
default int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
default int expectedMinLength(final ProtocolVersion protocolVersion) {
return -1;
}
/**
* @param byteBuf ByteBuf
* @param protocolVersion Protocol version of the player
* @return The maximum allowed length of the decoded packet
*/
default int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
default int expectedMaxLength(final ProtocolVersion protocolVersion) {
return -1;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,35 +20,30 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.maxTotalPacketsSent;
@RequiredArgsConstructor
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final ProtocolVersion protocolVersion;
private FallbackPacketRegistry.ProtocolRegistry registry;
@Setter
private FallbackPacketListener listener;
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
}
private int totalPacketsSent;
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
this.registry = registry.getProtocolRegistry(FallbackPacketRegistry.Direction.SERVERBOUND, protocolVersion);
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
@@ -60,8 +55,13 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
return;
}
// Don't allow the player to spam packets to overload netty
if (++totalPacketsSent > maxTotalPacketsSent) {
throw ProtocolUtil.DEBUG ? new DecoderException("Sent too many packets") : QuietDecoderException.INSTANCE;
}
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final int packetId = ProtocolUtil.readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// Skip the packet if it's not registered within Sonar's packet registry
@@ -70,27 +70,27 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
}
// Ensure that the packet isn't too large or too small
checkPacketSize(byteBuf, packet);
checkPacketSize(byteBuf.readableBytes(), packet);
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion);
} catch (Throwable throwable) {
throw new CorruptedFrameException("Failed to decode packet");
throw ProtocolUtil.DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
throw new CorruptedFrameException("Could not read packet to end");
throw ProtocolUtil.DEBUG ? new DecoderException("Could not read packet to end ("
+ byteBuf.readableBytes() + " bytes left)"): QuietDecoderException.INSTANCE;
}
// Let our verification handler process the packet
if (listener != null) {
listener.handle(packet);
// Make sure to let the timeout handler know about this packet
ctx.fireChannelRead(packet);
}
// Fire channel read to avoid timeout
ctx.fireChannelRead(packet);
} finally {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
@@ -98,16 +98,16 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
}
}
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
throw new CorruptedFrameException("Packet too large");
private void checkPacketSize(final int packetSize, final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(protocolVersion);
if (expectedMaxLen != -1 && packetSize > expectedMaxLen) {
throw ProtocolUtil.DEBUG ? new DecoderException("Packet too large: " + packetSize + " max: " + expectedMaxLen)
: QuietDecoderException.INSTANCE;
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
throw new CorruptedFrameException("Packet too small");
final int expectedMinLen = packet.expectedMinLength(protocolVersion);
if (expectedMinLen != -1 && packetSize < expectedMinLen) {
throw ProtocolUtil.DEBUG ? new DecoderException("Packet too small: " + packetSize + " min: " + expectedMinLen)
: QuietDecoderException.INSTANCE;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,37 +21,31 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@RequiredArgsConstructor
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
private final ProtocolVersion protocolVersion;
@Getter
private FallbackPacketRegistry packetRegistry;
private FallbackPacketRegistry.ProtocolRegistry protocolRegistry;
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
updateRegistry(LOGIN);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry packetRegistry) {
this.packetRegistry = packetRegistry;
this.protocolRegistry = packetRegistry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
this.protocolRegistry = packetRegistry.getProtocolRegistry(FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion);
}
@Override
protected void encode(final ChannelHandlerContext ctx,
final @NotNull FallbackPacket packet,
final ByteBuf out) throws Exception {
final FallbackPacket originalPacket = packet instanceof FallbackPacketSnapshot
? ((FallbackPacketSnapshot) packet).getOriginalPacket() : packet;
final @NotNull ByteBuf out) throws Exception {
final Class<? extends FallbackPacket> originalPacket = packet instanceof FallbackPacketSnapshot
? ((FallbackPacketSnapshot) packet).getOriginalPacketClass() : packet.getClass();
final int packetId = protocolRegistry.getPacketId(originalPacket);
writeVarInt(out, packetId);
ProtocolUtil.writeVarInt(out, packetId);
packet.encode(out, protocolVersion);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,8 +20,8 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
@@ -32,12 +32,14 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacke
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
HANDSHAKE {
@@ -65,11 +67,11 @@ public enum FallbackPacketRegistry {
map(0x01, MINECRAFT_1_20_2, true),
map(0x02, MINECRAFT_1_20_5, true));
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
map(0x02, MINECRAFT_1_20_2, false),
map(0x03, MINECRAFT_1_20_5, false));
map(0x02, MINECRAFT_1_20_2, true),
map(0x03, MINECRAFT_1_20_5, true));
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false),
map(0x04, MINECRAFT_1_20_5, false));
map(0x03, MINECRAFT_1_20_2, true),
map(0x04, MINECRAFT_1_20_5, true));
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
map(0x05, MINECRAFT_1_20_2, true),
map(0x07, MINECRAFT_1_20_5, true));
@@ -103,22 +105,24 @@ public enum FallbackPacketRegistry {
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true),
map(0x2B, MINECRAFT_1_20_5, true));
map(0x2B, MINECRAFT_1_20_5, true),
map(0x2C, MINECRAFT_1_21_2, true));
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x21, MINECRAFT_1_13, false),
map(0x20, MINECRAFT_1_14, false),
map(0x21, MINECRAFT_1_15, false),
map(0x20, MINECRAFT_1_16, false),
map(0x1F, MINECRAFT_1_16_2, false),
map(0x21, MINECRAFT_1_17, false),
map(0x1E, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, false));
map(0x00, MINECRAFT_1_7_2, true),
map(0x1F, MINECRAFT_1_9, true),
map(0x21, MINECRAFT_1_13, true),
map(0x20, MINECRAFT_1_14, true),
map(0x21, MINECRAFT_1_15, true),
map(0x20, MINECRAFT_1_16, true),
map(0x1F, MINECRAFT_1_16_2, true),
map(0x21, MINECRAFT_1_17, true),
map(0x1E, MINECRAFT_1_19, true),
map(0x20, MINECRAFT_1_19_1, true),
map(0x1F, MINECRAFT_1_19_3, true),
map(0x23, MINECRAFT_1_19_4, true),
map(0x24, MINECRAFT_1_20_2, true),
map(0x26, MINECRAFT_1_20_5, true),
map(0x27, MINECRAFT_1_21_2, true));
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
@@ -149,7 +153,8 @@ public enum FallbackPacketRegistry {
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true),
map(0x40, MINECRAFT_1_20_5, true));
map(0x40, MINECRAFT_1_20_5, true),
map(0x42, MINECRAFT_1_21_2, true));
clientbound.register(PlayerAbilitiesPacket.class, PlayerAbilitiesPacket::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
@@ -165,7 +170,8 @@ public enum FallbackPacketRegistry {
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true),
map(0x38, MINECRAFT_1_20_5, true));
map(0x38, MINECRAFT_1_20_5, true),
map(0x3A, MINECRAFT_1_21_2, true));
clientbound.register(ChunkDataPacket.class, ChunkDataPacket::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
@@ -180,7 +186,8 @@ public enum FallbackPacketRegistry {
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true),
map(0x27, MINECRAFT_1_20_5, true));
map(0x27, MINECRAFT_1_20_5, true),
map(0x28, MINECRAFT_1_21_2, true));
clientbound.register(RemoveEntitiesPacket.class, RemoveEntitiesPacket::new,
map(0x13, MINECRAFT_1_7_2, true),
map(0x30, MINECRAFT_1_9, true),
@@ -198,7 +205,8 @@ public enum FallbackPacketRegistry {
map(0x3E, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_19_4, true),
map(0x40, MINECRAFT_1_20_2, true),
map(0x42, MINECRAFT_1_20_5, true));
map(0x42, MINECRAFT_1_20_5, true),
map(0x47, MINECRAFT_1_21_2, true));
clientbound.register(SetPassengersPacket.class, SetPassengersPacket::new,
map(0x1B, MINECRAFT_1_7_2, true),
map(0x40, MINECRAFT_1_9, true),
@@ -213,27 +221,27 @@ public enum FallbackPacketRegistry {
map(0x59, MINECRAFT_1_19_4, true),
map(0x5B, MINECRAFT_1_20_2, true),
map(0x5D, MINECRAFT_1_20_3, true),
map(0x5F, MINECRAFT_1_20_5, true));
map(0x5F, MINECRAFT_1_20_5, true),
map(0x65, MINECRAFT_1_21_2, true));
clientbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
map(0x29, MINECRAFT_1_9, true),
map(0x2B, MINECRAFT_1_13, true),
map(0x2C, MINECRAFT_1_14, true),
map(0x2D, MINECRAFT_1_15, true),
map(0x2C, MINECRAFT_1_16, true),
map(0x2B, MINECRAFT_1_16_2, true),
map(0x2C, MINECRAFT_1_17, true),
map(0x29, MINECRAFT_1_19, true),
map(0x2B, MINECRAFT_1_19_1, true),
map(0x2A, MINECRAFT_1_19_3, true),
map(0x2E, MINECRAFT_1_19_4, true),
map(0x2F, MINECRAFT_1_20_2, true),
map(0x31, MINECRAFT_1_20_5, true),
map(0x33, MINECRAFT_1_21_2, true));
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
map(0x0E, MINECRAFT_1_7_2, true),
map(0x00, MINECRAFT_1_9, true),
map(0x01, MINECRAFT_1_19_4, true));
clientbound.register(PlayerInfoPacket.class, PlayerInfoPacket::new,
map(0x38, MINECRAFT_1_7_2, true),
map(0x2D, MINECRAFT_1_9, true),
map(0x2E, MINECRAFT_1_12_1, true),
map(0x30, MINECRAFT_1_13, true),
map(0x33, MINECRAFT_1_14, true),
map(0x34, MINECRAFT_1_15, true),
map(0x33, MINECRAFT_1_16, true),
map(0x32, MINECRAFT_1_16_2, true),
map(0x36, MINECRAFT_1_17, true),
map(0x34, MINECRAFT_1_19, true),
map(0x37, MINECRAFT_1_19_1, true),
map(0x36, MINECRAFT_1_19_3, true),
map(0x3A, MINECRAFT_1_19_4, true),
map(0x3C, MINECRAFT_1_20_2, true),
map(0x3E, MINECRAFT_1_20_5, true));
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
@@ -248,7 +256,8 @@ public enum FallbackPacketRegistry {
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true),
map(0x47, MINECRAFT_1_20_3, true),
map(0x49, MINECRAFT_1_20_5, true));
map(0x49, MINECRAFT_1_20_5, true),
map(0x4E, MINECRAFT_1_21_2, true));
clientbound.register(TransactionPacket.class, TransactionPacket::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
@@ -262,7 +271,8 @@ public enum FallbackPacketRegistry {
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true),
map(0x35, MINECRAFT_1_20_5, true));
map(0x35, MINECRAFT_1_20_5, true),
map(0x37, MINECRAFT_1_21_2, true));
clientbound.register(SetDefaultSpawnPositionPacket.class, SetDefaultSpawnPositionPacket::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
@@ -279,7 +289,8 @@ public enum FallbackPacketRegistry {
map(0x50, MINECRAFT_1_19_4, true),
map(0x52, MINECRAFT_1_20_2, true),
map(0x54, MINECRAFT_1_20_3, true),
map(0x56, MINECRAFT_1_20_5, true));
map(0x56, MINECRAFT_1_20_5, true),
map(0x5B, MINECRAFT_1_21_2, true));
clientbound.register(MapDataPacket.class, MapDataPacket::new,
map(0x34, MINECRAFT_1_7_2, true),
map(0x24, MINECRAFT_1_9, true),
@@ -293,7 +304,8 @@ public enum FallbackPacketRegistry {
map(0x25, MINECRAFT_1_19_3, true),
map(0x29, MINECRAFT_1_19_4, true),
map(0x2A, MINECRAFT_1_20_2, true),
map(0x2C, MINECRAFT_1_20_5, true));
map(0x2C, MINECRAFT_1_20_5, true),
map(0x2D, MINECRAFT_1_21_2, true));
clientbound.register(SetContainerSlotPacket.class, SetContainerSlotPacket::new,
map(0x2F, MINECRAFT_1_7_2, true),
map(0x16, MINECRAFT_1_9, true),
@@ -321,26 +333,30 @@ public enum FallbackPacketRegistry {
map(0x56, MINECRAFT_1_19_4, true),
map(0x58, MINECRAFT_1_20_2, true),
map(0x5A, MINECRAFT_1_20_3, true),
map(0x5C, MINECRAFT_1_20_5, true));
map(0x5C, MINECRAFT_1_20_5, true),
map(0x61, MINECRAFT_1_21_2, true));
clientbound.register(SystemChatPacket.class, SystemChatPacket::new,
map(0x02, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_13, false),
map(0x0F, MINECRAFT_1_15, false),
map(0x0E, MINECRAFT_1_16, false),
map(0x0F, MINECRAFT_1_17, false),
map(0x5F, MINECRAFT_1_19, false),
map(0x62, MINECRAFT_1_19_1, false),
map(0x60, MINECRAFT_1_19_3, false),
map(0x64, MINECRAFT_1_19_4, false),
map(0x67, MINECRAFT_1_20_2, false),
map(0x69, MINECRAFT_1_20_3, false),
map(0x6C, MINECRAFT_1_20_5, false));
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true),
map(0x0E, MINECRAFT_1_16, true),
map(0x0F, MINECRAFT_1_17, true),
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true),
map(0x69, MINECRAFT_1_20_3, true),
map(0x6C, MINECRAFT_1_20_5, true),
map(0x73, MINECRAFT_1_21_2, true));
clientbound.register(GameEventPacket.class, GameEventPacket::new,
map(0x20, MINECRAFT_1_20_3, true),
map(0x22, MINECRAFT_1_20_5, true));
map(0x22, MINECRAFT_1_20_5, true),
map(0x23, MINECRAFT_1_21_2, true));
clientbound.register(TransferPacket.class, TransferPacket::new,
map(0x73, MINECRAFT_1_20_5, true));
map(0x73, MINECRAFT_1_20_5, true),
map(0x7A, MINECRAFT_1_21_2, true));
clientbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
map(0x09, MINECRAFT_1_7_2, true),
map(0x37, MINECRAFT_1_9, true),
@@ -357,8 +373,37 @@ public enum FallbackPacketRegistry {
map(0x4D, MINECRAFT_1_19_4, true),
map(0x4F, MINECRAFT_1_20_2, true),
map(0x51, MINECRAFT_1_20_3, true),
map(0x53, MINECRAFT_1_20_5, true));
map(0x53, MINECRAFT_1_20_5, true),
map(0x63, MINECRAFT_1_21_2, true));
clientbound.register(EntityAnimationPacket.class, EntityAnimationPacket::new,
map(0x0B, MINECRAFT_1_7_2, true),
map(0x06, MINECRAFT_1_9, true),
map(0x05, MINECRAFT_1_16, true),
map(0x06, MINECRAFT_1_17, true),
map(0x03, MINECRAFT_1_19, true),
map(0x04, MINECRAFT_1_19_4, true),
map(0x03, MINECRAFT_1_20_2, true));
clientbound.register(UpdateTimePacket.class, UpdateTimePacket::new,
map(0x03, MINECRAFT_1_7_2, true),
map(0x44, MINECRAFT_1_9, true),
map(0x46, MINECRAFT_1_12, true),
map(0x47, MINECRAFT_1_12_1, true),
map(0x4A, MINECRAFT_1_13, true),
map(0x4E, MINECRAFT_1_14, true),
map(0x4F, MINECRAFT_1_15, true),
map(0x4E, MINECRAFT_1_16, true),
map(0x58, MINECRAFT_1_17, true),
map(0x59, MINECRAFT_1_18, true),
map(0x5C, MINECRAFT_1_19_1, true),
map(0x5A, MINECRAFT_1_19_3, true),
map(0x5E, MINECRAFT_1_19_4, true),
map(0x60, MINECRAFT_1_20_2, true),
map(0x62, MINECRAFT_1_20_3, true),
map(0x64, MINECRAFT_1_20_5, true),
map(0x6B, MINECRAFT_1_21_2, true));
serverbound.register(ClientTickEndPacket.class, ClientTickEndPacket::new,
map(0x0B, MINECRAFT_1_21_2, false));
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
map(0x09, MINECRAFT_1_7_2, false),
map(0x17, MINECRAFT_1_9, false),
@@ -371,7 +416,9 @@ public enum FallbackPacketRegistry {
map(0x28, MINECRAFT_1_19_1, false),
map(0x2B, MINECRAFT_1_20_2, false),
map(0x2C, MINECRAFT_1_20_3, false),
map(0x2F, MINECRAFT_1_20_5, false));
map(0x2F, MINECRAFT_1_20_5, false),
map(0x31, MINECRAFT_1_21_2, false),
map(0x33, MINECRAFT_1_21_4, false));
serverbound.register(PaddleBoatPacket.class, PaddleBoatPacket::new,
map(0x11, MINECRAFT_1_9, false),
map(0x12, MINECRAFT_1_12, false),
@@ -386,7 +433,8 @@ public enum FallbackPacketRegistry {
map(0x19, MINECRAFT_1_19_4, false),
map(0x1B, MINECRAFT_1_20_2, false),
map(0x1C, MINECRAFT_1_20_3, false),
map(0x1F, MINECRAFT_1_20_5, false));
map(0x1F, MINECRAFT_1_20_5, false),
map(0x21, MINECRAFT_1_21_2, false));
serverbound.register(PlayerInputPacket.class, PlayerInputPacket::new,
map(0x0C, MINECRAFT_1_7_2, false),
map(0x15, MINECRAFT_1_9, false),
@@ -401,7 +449,25 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19_4, false),
map(0x22, MINECRAFT_1_20_2, false),
map(0x23, MINECRAFT_1_20_3, false),
map(0x26, MINECRAFT_1_20_5, false));
map(0x26, MINECRAFT_1_20_5, false),
map(0x28, MINECRAFT_1_21_2, false),
map(0x29, MINECRAFT_1_21_4, false));
serverbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
map(0x10, MINECRAFT_1_9, false),
map(0x11, MINECRAFT_1_12, false),
map(0x10, MINECRAFT_1_12_1, false),
map(0x13, MINECRAFT_1_13, false),
map(0x15, MINECRAFT_1_14, false),
map(0x16, MINECRAFT_1_16, false),
map(0x15, MINECRAFT_1_17, false),
map(0x17, MINECRAFT_1_19, false),
map(0x18, MINECRAFT_1_19_1, false),
map(0x17, MINECRAFT_1_19_3, false),
map(0x18, MINECRAFT_1_19_4, false),
map(0x1A, MINECRAFT_1_20_2, false),
map(0x1B, MINECRAFT_1_20_3, false),
map(0x1E, MINECRAFT_1_20_5, false),
map(0x20, MINECRAFT_1_21_2, false));
serverbound.register(SystemChatPacket.class, SystemChatPacket::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x02, MINECRAFT_1_9, false),
@@ -411,7 +477,8 @@ public enum FallbackPacketRegistry {
map(0x04, MINECRAFT_1_19, false),
map(0x05, MINECRAFT_1_19_1, false),
map(0x05, MINECRAFT_1_19_3, false),
map(0x06, MINECRAFT_1_20_5, false));
map(0x06, MINECRAFT_1_20_5, false),
map(0x07, MINECRAFT_1_21_2, false));
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x0B, MINECRAFT_1_9, false),
@@ -427,7 +494,8 @@ public enum FallbackPacketRegistry {
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, false),
map(0x18, MINECRAFT_1_20_5, false));
map(0x18, MINECRAFT_1_20_5, false),
map(0x1A, MINECRAFT_1_21_2, false));
serverbound.register(ClientInformationPacket.class, ClientInformationPacket::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
@@ -439,7 +507,8 @@ public enum FallbackPacketRegistry {
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false),
map(0x0A, MINECRAFT_1_20_5, false));
map(0x0A, MINECRAFT_1_20_5, false),
map(0x0C, MINECRAFT_1_21_2, false));
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
@@ -453,7 +522,8 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false),
map(0x12, MINECRAFT_1_21, false));
map(0x12, MINECRAFT_1_21, false),
map(0x14, MINECRAFT_1_21_2, false));
serverbound.register(SetPlayerOnGround.class, SetPlayerOnGround::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
@@ -469,7 +539,8 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false),
map(0x1A, MINECRAFT_1_20_3, false),
map(0x1D, MINECRAFT_1_20_5, false));
map(0x1D, MINECRAFT_1_20_5, false),
map(0x1F, MINECRAFT_1_21_2, false));
serverbound.register(SetPlayerPositionPacket.class, SetPlayerPositionPacket::new,
map(0x04, MINECRAFT_1_7_2, false),
map(0x0C, MINECRAFT_1_9, false),
@@ -485,7 +556,8 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false),
map(0x17, MINECRAFT_1_20_3, false),
map(0x1A, MINECRAFT_1_20_5, false));
map(0x1A, MINECRAFT_1_20_5, false),
map(0x1C, MINECRAFT_1_21_2, false));
serverbound.register(SetPlayerRotationPacket.class, SetPlayerRotationPacket::new,
map(0x05, MINECRAFT_1_7_2, false),
map(0x0E, MINECRAFT_1_9, false),
@@ -501,7 +573,8 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_19_4, false),
map(0x18, MINECRAFT_1_20_2, false),
map(0x19, MINECRAFT_1_20_3, false),
map(0x1C, MINECRAFT_1_20_5, false));
map(0x1C, MINECRAFT_1_20_5, false),
map(0x1E, MINECRAFT_1_21_2, false));
serverbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
@@ -517,7 +590,8 @@ public enum FallbackPacketRegistry {
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false),
map(0x18, MINECRAFT_1_20_3, false),
map(0x1B, MINECRAFT_1_20_5, false));
map(0x1B, MINECRAFT_1_20_5, false),
map(0x1D, MINECRAFT_1_21_2, false));
serverbound.register(ConfirmTeleportationPacket.class, ConfirmTeleportationPacket::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(TransactionPacket.class, TransactionPacket::new,
@@ -534,7 +608,24 @@ public enum FallbackPacketRegistry {
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false),
map(0x24, MINECRAFT_1_20_3, false),
map(0x27, MINECRAFT_1_20_5, false));
map(0x27, MINECRAFT_1_20_5, false),
map(0x29, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_4, false));
serverbound.register(AnimationPacket.class, AnimationPacket::new,
map(0x0A, MINECRAFT_1_7_2, false),
map(0x1A, MINECRAFT_1_9, false),
map(0x1D, MINECRAFT_1_12, false),
map(0x27, MINECRAFT_1_13, false),
map(0x2A, MINECRAFT_1_14, false),
map(0x2B, MINECRAFT_1_16, false),
map(0x2C, MINECRAFT_1_16_2, false),
map(0x2E, MINECRAFT_1_19, false),
map(0x2F, MINECRAFT_1_19_1, false),
map(0x32, MINECRAFT_1_20_2, false),
map(0x33, MINECRAFT_1_20_3, false),
map(0x36, MINECRAFT_1_20_5, false),
map(0x38, MINECRAFT_1_21_2, false),
map(0x3A, MINECRAFT_1_21_4, false));
}
};
@@ -551,30 +642,28 @@ public enum FallbackPacketRegistry {
return (direction == Direction.SERVERBOUND ? serverbound : clientbound).getProtocolRegistry(version);
}
public static class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
public static final class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> registries = new EnumMap<>(ProtocolVersion.class);
PacketRegistry() {
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (final ProtocolVersion version : ProtocolVersion.values()) {
if (!version.isUnknown()) {
mutableVersions.put(version, new ProtocolRegistry(version));
for (final ProtocolVersion protocolVersion : ProtocolVersion.values()) {
if (!protocolVersion.isUnknown()) {
registries.put(protocolVersion, new ProtocolRegistry(protocolVersion));
}
}
this.versions = Collections.unmodifiableMap(mutableVersions);
}
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
final ProtocolRegistry registry = versions.get(version);
private @NotNull ProtocolRegistry getProtocolRegistry(final @NotNull ProtocolVersion protocolVersion) {
final ProtocolRegistry registry = registries.get(protocolVersion);
if (registry == null) {
throw new IllegalArgumentException("Could not find data for protocol version " + version);
throw new IllegalArgumentException("Could not find data for protocol version " + protocolVersion);
}
return registry;
}
<P extends FallbackPacket> void register(final Class<P> clazz,
final Supplier<P> packetSupplier,
final PacketMapping @NotNull ... mappings) {
private <T extends FallbackPacket> void register(final @NotNull Class<T> clazz,
final @NotNull Supplier<T> supplier,
final PacketMapping @NotNull ... mappings) {
if (mappings.length == 0) {
throw new IllegalArgumentException("At least one mapping must be provided.");
}
@@ -584,21 +673,21 @@ public enum FallbackPacketRegistry {
final PacketMapping next = (i + 1 < mappings.length) ? mappings[i + 1] : current;
final ProtocolVersion from = current.protocolVersion;
final ProtocolVersion to = getProtocolVersion(current, next);
final ProtocolVersion to = current == next ? LATEST_VERSION : next.protocolVersion;
for (final ProtocolVersion protocol : EnumSet.range(from, to)) {
if (protocol == to && next != current) {
for (final ProtocolVersion protocolVersion : EnumSet.range(from, to)) {
if (protocolVersion == to && next != current) {
break;
}
final ProtocolRegistry registry = versions.get(protocol);
final ProtocolRegistry registry = registries.get(protocolVersion);
if (registry == null) {
throw new IllegalArgumentException("Unknown protocol version " + from);
throw new IllegalArgumentException("Unknown protocol version " + protocolVersion);
}
if (registry.packetIdToSupplier.containsKey(current.id)) {
if (registry.packetIdToSupplier.containsKey(current.packetId)) {
throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName()
+ " with id " + current.id + " for " + registry.protocolVersion
+ " with id " + current.packetId + " for " + registry.protocolVersion
+ " because another packet is already registered");
}
@@ -608,18 +697,12 @@ public enum FallbackPacketRegistry {
}
if (!current.encodeOnly) {
registry.packetIdToSupplier.put(current.id, packetSupplier);
registry.packetIdToSupplier.put(current.packetId, supplier);
}
registry.packetClassToId.put(clazz, current.id);
registry.packetClassToId.put(clazz, current.packetId);
}
}
}
@NotNull
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
final @NotNull PacketMapping next) {
return current == next ? LATEST_VERSION : next.protocolVersion;
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@@ -630,43 +713,38 @@ public enum FallbackPacketRegistry {
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
new HashMap<>(16, 0.5f);
public FallbackPacket createPacket(final int id) {
final Supplier<? extends FallbackPacket> supplier = packetIdToSupplier.get(id);
if (supplier == null) {
return null;
}
return supplier.get();
public FallbackPacket createPacket(final int packetId) {
final var supplier = packetIdToSupplier.get(packetId);
return supplier == null ? null : supplier.get();
}
public int getPacketId(final @NotNull FallbackPacket packet) {
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
if (id == Integer.MIN_VALUE) {
throw new IllegalArgumentException("Could not find packet");
public int getPacketId(final @NotNull Class<? extends FallbackPacket> clazz) {
final int packetId = packetClassToId.getOrDefault(clazz, -1);
if (packetId == -1) {
throw new IllegalArgumentException("Could not find packet ID for " + clazz.getSimpleName());
}
return id;
return packetId;
}
}
@Data
@ToString
public static final class PacketMapping {
private final int id;
private final int packetId;
private final ProtocolVersion protocolVersion;
private final boolean encodeOnly;
PacketMapping(final int id,
PacketMapping(final int packetId,
final ProtocolVersion protocolVersion,
final boolean encodeOnly) {
this.id = id;
this.packetId = packetId;
this.protocolVersion = protocolVersion;
this.encodeOnly = encodeOnly;
}
}
private static @NotNull PacketMapping map(final int id,
private static @NotNull PacketMapping map(final int packetId,
final ProtocolVersion protocolVersion,
final boolean encodeOnly) {
return new PacketMapping(id, protocolVersion, encodeOnly);
return new PacketMapping(packetId, protocolVersion, encodeOnly);
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.EncoderException;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.ID_TO_PROTOCOL_CONSTANT;
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/PacketSnapshot.java
public final class FallbackPacketSnapshot implements FallbackPacket {
private final IntObjectMap<Integer> mappings = new IntObjectHashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
private final IntObjectMap<byte[]> cachedBytes = new IntObjectHashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
@Getter
private final Class<? extends FallbackPacket> originalPacketClass;
public FallbackPacketSnapshot(final @NotNull FallbackPacket originalPacket) {
this.originalPacketClass = originalPacket.getClass();
final IntObjectMap<Integer> hashedData = new IntObjectHashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
for (final ProtocolVersion protocolVersion : ID_TO_PROTOCOL_CONSTANT.values()) {
// Allocate a buffer for each protocol version
final ByteBuf byteBuf = Unpooled.buffer();
try {
try {
originalPacket.encode(byteBuf, protocolVersion);
} catch (Throwable throwable) {
Sonar.get0().getLogger().error("Could not encode packet {} for version {}: {}",
originalPacket, protocolVersion, throwable);
break;
}
final Integer protocol = protocolVersion.getProtocol();
final Integer hash = byteBuf.hashCode();
final Integer hashed = hashedData.getOrDefault(hash, -1);
if (hashed != -1) {
mappings.put(protocol, hashed);
} else {
hashedData.put(hash, protocol);
mappings.put(protocol, protocol);
// Cache the raw bytes of the encoded packet
final byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
cachedBytes.put(protocol, bytes);
}
} finally {
// Make sure to release the buffer to avoid memory leaks
byteBuf.release();
}
}
}
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
final Integer hash = mappings.get(protocolVersion.getProtocol());
if (hash == null) {
throw ProtocolUtil.DEBUG ? new EncoderException("Unable to find hash") : QuietDecoderException.INSTANCE;
}
final byte[] bytes = cachedBytes.get(hash);
if (bytes == null) {
throw ProtocolUtil.DEBUG ? new EncoderException("Unable to find cached packet") : QuietDecoderException.INSTANCE;
}
byteBuf.writeBytes(bytes);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,284 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.IntBinaryTag;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.event.impl.CaptchaGenerationStartEvent;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionType;
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
import xyz.jonesdev.sonar.common.fallback.protocol.item.ItemType;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.util.ComponentHolder;
import java.util.Random;
import java.util.UUID;
@UtilityClass
public class FallbackPreparer {
private final Random RANDOM = new Random();
public static final int PLAYER_ENTITY_ID = RANDOM.nextInt();
public static final int VEHICLE_ENTITY_ID = RANDOM.nextInt();
public static final BlockType[] POSSIBLE_BLOCK_TYPES = BlockType.values();
public final FallbackPacket[] BLOCKS_PACKETS = new FallbackPacket[POSSIBLE_BLOCK_TYPES.length];
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (vanilla protocol maximum)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
public final int PLATFORM_Y_POSITION = 1 + RANDOM.nextInt(255); // 255 is the maximum Y position
public final int IN_AIR_Y_POSITION = 3000 + RANDOM.nextInt(500); // Random high altitude
public final int FIRST_TELEPORT_ID = RANDOM.nextInt();
public final int SECOND_TELEPORT_ID = RANDOM.nextInt();
public final int PRE_JOIN_KEEP_ALIVE_ID = RANDOM.nextInt();
private final int MAP_SLOT = RANDOM.nextInt(9);
public static final FallbackPacket CAPTCHA_SET_CONTAINER_SLOT = new SetContainerSlotPacket(
0, 36 + MAP_SLOT, 1, ItemType.FILLED_MAP, CompoundBinaryTag.builder()
.put("map", IntBinaryTag.intBinaryTag(0)) // map id
.build());
public final static FallbackPacket CAPTCHA_HELD_ITEM_SLOT = new SetHeldItemPacket(MAP_SLOT);
public final FallbackPacket DEFAULT_ABILITIES = new PlayerAbilitiesPacket(0x00, 0, 0);
public final FallbackPacket NO_MOVE_ABILITIES = new PlayerAbilitiesPacket(0x02, 0, 0);
public final FallbackPacket NO_MOVE_ABILITIES_BEDROCK = new PlayerAbilitiesPacket(0x06, 0, 0);
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, 10000, SPAWN_Z_POSITION, 0, 90, 0, 0, false, false, true));
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
public final FallbackPacket PRE_JOIN_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(PRE_JOIN_KEEP_ALIVE_ID));
public final FallbackPacket[] REGISTRY_SYNC_1_20 = new FallbackPacket[] {
new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null))};
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20_5);
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
public final FallbackPacket[] REGISTRY_SYNC_1_21_2 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21_2);
public final FallbackPacket[] REGISTRY_SYNC_1_21_4 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21_4);
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
public final static FallbackPacket INVALID_HELD_ITEM_SLOT = new SetHeldItemPacket(-1);
public final FallbackPacket RANDOM_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(RANDOM.nextInt()));
public static final FallbackPacket SPAWN_BOAT_ENTITY = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, 0, 0, 0));
public static final FallbackPacket SPAWN_MINECART_ENTITY = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_ENTITY_ID, EntityType.MINECART, SPAWN_X_POSITION, IN_AIR_Y_POSITION - 16, SPAWN_Z_POSITION,
0, 0, 0, 0));
public static final FallbackPacket REMOVE_VEHICLE = new RemoveEntitiesPacket(VEHICLE_ENTITY_ID);
public static final FallbackPacket SET_VEHICLE_PASSENGERS = new FallbackPacketSnapshot(
new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
public FallbackPacket loginSuccess;
public FallbackPacket welcomeMessage;
public FallbackPacket enterCodeMessage;
public FallbackPacket incorrectCaptcha;
public static FallbackPacket joinGame;
public FallbackPacket defaultSpawnPosition;
public FallbackPacket spawnPosition;
public FallbackPacket fallStartPosition;
public FallbackPacket fallStartPositionLegacy;
public static FallbackPacket transferToOrigin;
public FallbackPacket blacklisted;
public FallbackPacket alreadyQueued;
public FallbackPacket alreadyVerifying;
public FallbackPacket reconnectedTooFast;
public FallbackPacket protocolBlacklisted;
public FallbackPacket unsupportedVersion;
public FallbackPacket tooManyOnlinePerIP;
public FallbackPacket[] xpCountdown;
public FallbackPacket updateTime;
public static int maxMovementTick, dynamicSpawnYPosition, maxTotalPacketsSent;
public void prepare() {
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
Sonar.get0().getLogger().info("Preloading all registered packets...");
//noinspection ResultOfMethodCallIgnored
FallbackPacketRegistry.values();
// Prepare LoginSuccess packet with capped username to 16 characters
final UUID uuid = UUID.randomUUID();
String username = Sonar.get0().getConfig().getGeneralConfig().getString("verification.cached-username");
if (username.length() > 16) {
username = username.substring(0, 16);
}
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(uuid, username, true));
// Prepare JoinGame packet
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
Sonar.get0().getConfig().getVerification().getGamemode().getId(),
-1, 0, 0,
RANDOM.nextInt(3), 1, 0, 0,
new String[]{"minecraft:overworld"}, "minecraft:overworld", "flat",
DimensionType.OVERWORLD, RANDOM.nextLong() & 1337,
false, true, false,
false, false, false, true));
// Prepare the gravity check
maxMovementTick = Sonar.get0().getConfig().getVerification().getGravity().getMaxMovementTicks();
double motionY = 0, fallDistance = 0;
for (int i = 0; i < maxMovementTick; i++) {
motionY = (motionY - 0.1) * 0.98f;
fallDistance += Math.abs(motionY);
}
// Set the dynamic block and collide Y position based on the maximum fall distance
dynamicSpawnYPosition = PLATFORM_Y_POSITION + (int) Math.ceil(fallDistance);
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, 0, FIRST_TELEPORT_ID, 0, false, false, true));
fallStartPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition - IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, -90, SECOND_TELEPORT_ID, 1 << 1, false, false, true));
fallStartPositionLegacy = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0, -90, 0, 0, false, false, true));
// Prepare collision platform positions
final int length = BLOCKS_PER_ROW / 2;
for (int i = 0; i < BLOCKS_PACKETS.length; i++) {
final BlockUpdate[] blocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
int index = 0;
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
x + length, PLATFORM_Y_POSITION, z + length, 0, 0);
blocks[index++] = new BlockUpdate(position, POSSIBLE_BLOCK_TYPES[i]);
}
}
BLOCKS_PACKETS[i] = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, blocks));
}
// Prepare disconnect packets during login
blacklisted = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getVerification().getBlacklisted(), true));
alreadyVerifying = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getVerification().getAlreadyVerifying(), true));
alreadyQueued = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getVerification().getAlreadyQueued(), true));
protocolBlacklisted = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getVerification().getProtocolBlacklisted(), true));
reconnectedTooFast = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getVerification().getTooFastReconnect(), true));
unsupportedVersion = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getVerification().getUnsupportedVersion(), true));
tooManyOnlinePerIP = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get0().getConfig().getTooManyOnlinePerIp(), true));
// Prepare transfer packet
if (Sonar.get0().getConfig().getGeneralConfig().getBoolean("verification.transfer.enabled")) {
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
Sonar.get0().getConfig().getGeneralConfig().getString("verification.transfer.destination-host"),
Sonar.get0().getConfig().getGeneralConfig().getInt("verification.transfer.destination-port")));
} else {
transferToOrigin = null;
}
// Prepare update time packet
final int timeOfDay = Sonar.get0().getConfig().getVerification().getTimeOfDay();
if (timeOfDay != 1000) {
updateTime = new UpdateTimePacket(0L, timeOfDay, false);
} else {
updateTime = null;
}
// If the welcome message is empty, we don't need to send a message to the player
final String welcome = Sonar.get0().getConfig().getMessagesConfig().getString("verification.welcome");
if (welcome.isEmpty()) {
welcomeMessage = null;
} else {
welcomeMessage = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
MiniMessage.miniMessage().deserialize(welcome,
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix())))));
}
if (Sonar.get0().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|| Sonar.get0().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
// Prepare CAPTCHA messages
enterCodeMessage = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
MiniMessage.miniMessage().deserialize(
Sonar.get0().getConfig().getMessagesConfig().getString("verification.captcha.enter"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix())))));
incorrectCaptcha = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
MiniMessage.miniMessage().deserialize(
Sonar.get0().getConfig().getMessagesConfig().getString("verification.captcha.incorrect"),
Placeholder.component("prefix", Sonar.get0().getConfig().getPrefix())))));
// Prepare countdown
xpCountdown = new FallbackPacket[Sonar.get0().getConfig().getVerification().getMap().getMaxDuration() / 1000];
for (int i = 0; i < xpCountdown.length; i++) {
final float bar = (float) i / xpCountdown.length;
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, 0));
}
// Update the CAPTCHA generator if necessary
final CaptchaGenerationStartEvent generationStartEvent = new CaptchaGenerationStartEvent(
Sonar.get0().getFallback().getCaptchaGenerator());
Sonar.get0().getEventManager().publish(generationStartEvent);
if (generationStartEvent.getCaptchaGenerator() == null
|| generationStartEvent.getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
Sonar.get0().getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(
Sonar.get0().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
Sonar.get0().getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare CAPTCHA answers
CaptchaPreparer.prepare();
} else {
// Throw away if not needed
enterCodeMessage = null;
incorrectCaptcha = null;
xpCountdown = null;
Sonar.get0().getFallback().setCaptchaGenerator(null);
}
maxTotalPacketsSent = maxMovementTick + 2
+ (xpCountdown == null ? 0 : xpCountdown.length) * 20 + 5
+ Sonar.get0().getConfig().getVerification().getVehicle().getMinimumPackets() * 4
+ Sonar.get0().getConfig().getVerification().getMap().getMaxTries()
+ 150 /* some arbitrary leeway */;
}
public static FallbackPacket[] getRegistryPackets(final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21_4)) {
return REGISTRY_SYNC_1_21_4;
} else if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21_2)) {
return REGISTRY_SYNC_1_21_2;
} else if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21)) {
return REGISTRY_SYNC_1_21;
} else if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_20_5)) {
return REGISTRY_SYNC_1_20_5;
}
return REGISTRY_SYNC_1_20;
}
}
@@ -0,0 +1,330 @@
/*
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// TODO: load mappings from a separate file
@Getter
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
//STONE(protocolVersion -> 1, 1),
ENCHANTMENT_TABLE(protocolVersion -> {
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 116;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 4612;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 4613;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 5116;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 5132;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 5136;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 5333;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 5719;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 7159;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 7385;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 7389;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 7619;
}
return 8163;
}, protocolVersion -> (double) 0.75f),
TRAPDOOR(protocolVersion -> {
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_7_6)) {
return 96;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 167;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 6509;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 6510;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 7016;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 7552;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 7556;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 7802;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 8293;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 9937;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 10269;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
return 10273;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 10414;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 10749;
}
return 11293;
}, protocolVersion -> 0.1875),
END_PORTAL_FRAME(protocolVersion -> {
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 120;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 4633;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 4634;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 5137;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 5153;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 5157;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 5358;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 5744;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 7184;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 7410;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 7414;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 7644;
}
return 8185;
}, protocolVersion -> 0.8125),
DAYLIGHT_SENSOR(protocolVersion -> {
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 151;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 5651;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 5652;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 6158;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 6694;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 6698;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 6916;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 7327;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 8811;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 9063;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
return 9067;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 9207;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 9462;
}
return 10006;
}, protocolVersion -> 0.375),
COBBLESTONE_WALL(protocolVersion -> {
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 139;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 5196;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 5197;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 5700;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 5660;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 5664;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 5866;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 6252;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 7692;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 7918;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 7922;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 8152;
}
return 8696;
}, protocolVersion -> 1.5),
STONE_SLABS(protocolVersion -> {
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 44;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 7296;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 7297;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 7809;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 8345;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 8349;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 8595;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 9092;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 10748;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 11086;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
return 11090;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 11231;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 11566;
}
return 12110;
}, protocolVersion -> 0.5),
WHITE_CARPET(protocolVersion -> {
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
return 171;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
return 6823;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
return 6824;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
return 7330;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
return 7866;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
return 7870;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
return 8116;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
return 8607;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
return 10251;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
return 10583;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
return 10587;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
return 10728;
}
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21_2)) {
return 11063;
}
return 11607;
}, protocolVersion -> protocolVersion.compareTo(MINECRAFT_1_8) < 0 ? 0 : 0.0625);
private final Function<ProtocolVersion, Integer> id;
private final Function<ProtocolVersion, Double> blockHeight;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,18 +19,17 @@ package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
@Getter
public final class BlockUpdate {
private final @NotNull BlockPosition position;
private final BlockType type;
private final BlockPosition position;
private final BlockType blockType;
private final int legacyBlockState, blockState;
public BlockUpdate(final @NotNull BlockPosition position, final BlockType type) {
public BlockUpdate(final @NotNull BlockPosition position, final BlockType blockType) {
this.position = position;
this.type = type;
this.blockType = blockType;
final int x = position.getX() - (position.getChunkX() << 4);
final int y = position.getY();
final int z = position.getZ() - (position.getChunkZ() << 4);
@@ -39,7 +38,6 @@ public final class BlockUpdate {
}
@Getter
@ToString
@RequiredArgsConstructor
public static final class BlockPosition {
private final int x, y, z;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Sonar Contributors
* Copyright (C) 2025 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,10 +18,8 @@
package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.BinaryTagIO;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.ListBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
@@ -30,43 +28,37 @@ import java.util.Objects;
@UtilityClass
public final class DimensionRegistry {
public final DimensionInfo DEFAULT_DIMENSION_1_16;
public final DimensionInfo DEFAULT_DIMENSION_1_18_2;
public final @NotNull CompoundBinaryTag CODEC_1_16;
public final @NotNull CompoundBinaryTag CODEC_1_18_2;
public final @NotNull CompoundBinaryTag CODEC_1_19;
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
public final @NotNull CompoundBinaryTag CODEC_1_20;
public final @NotNull CompoundBinaryTag CODEC_1_21;
public final @NotNull CompoundBinaryTag OLD_CODEC;
public final CompoundBinaryTag CODEC_1_16;
public final CompoundBinaryTag CODEC_1_16_2;
public final CompoundBinaryTag CODEC_1_18_2;
public final CompoundBinaryTag CODEC_1_19;
public final CompoundBinaryTag CODEC_1_19_1;
public final CompoundBinaryTag CODEC_1_19_4;
public final CompoundBinaryTag CODEC_1_20;
public final CompoundBinaryTag CODEC_1_20_5;
public final CompoundBinaryTag CODEC_1_21;
public final CompoundBinaryTag CODEC_1_21_2;
public final CompoundBinaryTag CODEC_1_21_4;
static {
CODEC_1_16 = getCodec("codec_1_16.nbt");
CODEC_1_16_2 = getCodec("codec_1_16_2.nbt");
CODEC_1_18_2 = getCodec("codec_1_18_2.nbt");
CODEC_1_19 = getCodec("codec_1_19.nbt");
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
CODEC_1_20 = getCodec("codec_1_20.nbt");
CODEC_1_20_5 = getCodec("codec_1_20_5.nbt");
CODEC_1_21 = getCodec("codec_1_21.nbt");
OLD_CODEC = getCodec("codec_old.nbt");
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
DEFAULT_DIMENSION_1_18_2 = getDimension(CODEC_1_18_2);
}
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
CODEC_1_21_2 = getCodec("codec_1_21_2.nbt");
CODEC_1_21_4 = getCodec("codec_1_21_4.nbt");
}
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/assets/codecs/" + fileName)) {
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
return BinaryTagIO.unlimitedReader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
Sonar.get0().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
throw new IllegalStateException(throwable);
}
}
Loaded 100 of 264 files, more files were not shown because too many files have changed in this diff. Show more