Compare commits

..

127 Commits

Author SHA1 Message Date
Chaoscaot cdeabe91d0 Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build failed
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2026-05-21 08:23:49 +02:00
Aaron 9c0c9b0218 fix: outdated client message (#1763) 2026-05-09 08:27:52 +01:00
Wouter Gritter ad8de4361c Fix wrong logs in exception(Throwable) (#1784) 2026-05-01 19:13:51 +01:00
Shane Freeder f712997dd7 Seperate client/serverbound payload limits 2026-04-17 21:03:04 +01:00
Shane Freeder a6d97e28ad Validate compression claim 2026-04-13 13:39:57 +01:00
booky 339a4c1887 Fix some small protocol inconsistencies (#1772) 2026-04-12 20:18:42 +01:00
Aaron 1a41b77ccb Add 26.1.2 version string to 26.1 protocol version (#1769) 2026-04-10 18:01:39 +02:00
Shane Freeder e834af9cf1 Increase max ratio
Older versions of the game, and creative mode, send itemstacks to the server
when dealing with itemstacks, annoying, the compression algo used is good at
backreferencing, which means that compressed data can balloon pretty well.

64 should more than cover most cases of legit data, we could probably be more
harsh here, but this is likely a fine balance between avoiding bombs and not
erring out on legit data.
2026-04-09 19:04:02 +01:00
Shane Freeder affc1d6e08 Extend debug logging to client play/config handlers 2026-04-09 19:03:33 +01:00
R00tB33rMan 6ce432e4ae Handle configuration/transition-phase disconnects as connection errors to prevent bricking older client versions (#1753) 2026-04-09 15:50:01 +01:00
Harold b1a1b8bda3 use outbound only queueing when reentering configuration (#1747)
* use outbound only queueing and guard serverbound forwarding during reconfiguration
2026-04-08 23:27:02 +01:00
Shane Freeder 6f01587318 Appease checkstyle 2026-04-08 21:19:27 +01:00
Shane Freeder 308ce6d992 Merge branch 'cat/limits' into dev/3.0.0 2026-04-08 21:12:52 +01:00
Shane Freeder 9890c429c6 Add compression ratio limiter 2026-04-08 21:12:41 +01:00
Shane Freeder 0219993c8a Add basic packet limiter 2026-04-08 20:41:45 +01:00
Shane Freeder f6d48c90f9 reduce clientbound compression limits 2026-04-08 20:41:09 +01:00
Aaron ab99bde9d6 Minecraft 26.1.1 (#1760)
* 26.1.1-rc-1

* 26.1.1
2026-04-01 15:20:30 +01:00
Timon Seidel 2cf181df0c [ci skip] chore(readme): add note regarding localisation (#1759) 2026-03-31 11:32:54 -05:00
Aaron d11511c184 Minecraft 26.1 (#1739)
* 26.1-snapshot-10

* 26.1-snapshot-11

* 26.1-pre-1

* 26.1-pre-2

* 26.1-pre-3

* 26.1-rc-1

* 26.1-rc-2

* 26.1
2026-03-21 23:14:28 -05:00
Wouter Gritter 99bd030996 Implement missing writabilityChanged() and add backlog logging with BACKPRESSURE_LOG to all writabilityChanged() implementations. (#1745) 2026-03-18 18:23:01 +00:00
booky 5017f8c9f2 Add more protocol safeguards (#1743)
* Add max length to more config phase packets

* Ensure all packets during non-play state are known

* Limit maximum size of play inbound packet queue

Co-authored-by: Dwarslooper <dwarslooper2910@gmx.de>

* Fix checkstyle

---------

Co-authored-by: Dwarslooper <dwarslooper2910@gmx.de>
2026-03-18 12:04:31 +00:00
Nassim Jahnke e8b64aa6c0 Add two more pre-sizing checks 2026-03-15 09:04:34 +01:00
Nassim Jahnke 470cd7a9d2 checkstyle 2026-03-15 08:44:31 +01:00
Nassim Jahnke 0783b1d4e4 Add remaining pre-sizing checks 2026-03-15 08:39:32 +01:00
Shane Freeder 747cc8984f Bump netty to 4.2.10.Final 2026-03-08 20:00:28 +00:00
Riley Park e0db25664f Revert "Add various missing jd, bump remaining deps (#1718)"
This reverts commit 7d0c002f89.
2026-03-03 07:22:59 -08:00
Noah 6aff78728c [ci skip] fix: typo in ServerPreConnectEvent.ServerResult#denied javadoc (#1735) 2026-02-21 14:26:49 -05:00
Wouter Gritter c2fd3c07ac Introduce SchedulerBackend to fix VelocitySchedulerTest intermittent failure (#1728) 2026-02-11 16:21:03 -05:00
Rocco 2535751cd9 Add server-id hash to LoginEvent (#1027) 2026-02-10 19:59:34 +01:00
Nassim Jahnke 7e01491e2f Promote build channel to stable 2026-01-31 11:34:51 +01:00
Emil 14160e1988 feat: Implement SkinSource for Player and GameProfile (#1721) 2026-01-30 13:09:27 -05:00
Pedro 9bfe19f795 [ci skip] Replace docs.advntr.dev to docs.papermc.io in jd (#1720) 2026-01-30 12:58:44 -05:00
R00tB33rMan 7d0c002f89 Add various missing jd, bump remaining deps (#1718) 2026-01-29 14:31:50 +01:00
Riley Park 5320aae5d9 fix #1695: add "velocity.legacyChatMaxServerboundLength" system property to allow overriding default legacy chat max length 2026-01-25 21:20:47 -08:00
Emil fe8dc5eaa2 fix: TranslatableMapper not using fallback translation (#1716) 2026-01-25 15:56:55 -05:00
Adrian eb2bea79ba Implement MiniMessage Velocity translations (#1108)
* Implement MiniMessage Velocity translations

* Implemented migration from old language format

* Updated year in default translations

* Fixed compilation

* Use MiniMessageTranslationStore

* Simplify MiniMessageTranslationsMigration#shouldMigrate

* Added support to named placeholders

* Updated translation arguments to MiniMessage translation placeholders
2026-01-25 14:06:03 -05:00
Adrian 75ecb64159 Update minimum Java version to 21 (#1649) 2026-01-25 19:22:54 +01:00
Nassim Jahnke d69431a08e Update dependencies (#1717) 2026-01-25 19:13:08 +01:00
Nassim Jahnke 7e4f37b5f5 Bump version to 3.5.0-SNAPSHOT 2026-01-25 18:53:56 +01:00
Nassim Jahnke 1df79a403b Update fill 2026-01-25 18:51:00 +01:00
Nassim Jahnke 6b1ea78ff7 release 3.4.0 2026-01-25 18:20:04 +01:00
mason 3022793418 Fix ByteBuf memory leak in MinecraftVarintFrameDecoder (#1715)
- Reset buffer reader index on exception to prevent memory leaks when packet decoding fails.
2026-01-21 18:56:22 +00:00
Warrior 21671daebe Provide an IDE pattern hint for plugin IDs (#1712)
* Provide an IDE pattern hint for plugin IDs

* order my imports the right way
2026-01-19 12:43:06 +00:00
Shane Freeder 372a3b28bd Conformity 2026-01-08 17:13:08 +00:00
Shane Freeder a03bd884aa Restrict empty packet frames from clients 2026-01-08 16:53:58 +00:00
Aaron 4bc3f00424 bump adventure to 4.26.1 (#1697) 2025-12-27 13:41:43 -05:00
Adrian 8f65a81420 Minecraft 1.21.11 (#1690)
* 1.21.11-pre2

I have not identified any changes that affect us at this time

* 1.21.11-pre3

* 1.21.11-pre4

* 1.21.11-pre5

* 1.21.11-rc1

* 1.21.11-rc2

* Minecraft 1.21.11 release support
2025-12-07 15:28:00 -05:00
Adrian Gonzales 6cc1be7746 [ci skip] Replaced slf4j javadocs provider with javadocs.dev 2025-11-09 15:23:36 -05:00
SzymON/OFF a046f70075 Fix exception message formatting in command invocation (#1687) 2025-11-09 19:59:06 +00:00
ZX夏夜之风 75d68115ef feat: PlayerChannelUnregisterEvent (#1686)
* feat: PlayerChannelUnregisterEvent

* style: fix checkstyle issues
2025-11-09 11:34:58 -05:00
Chaoscaot babb22bf88 Remove duplicate mapping for MINECRAFT_1_21_2 in StateRegistry
SteamWarCI Build successful
2025-11-09 01:51:08 +01:00
Chaoscaot 468127996c Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build failed
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2025-11-09 01:46:27 +01:00
Adrian b6b6b20fe9 Generate a new forwarding secret file if the file is deleted (#1671)
* Generate a new forwarding secret file if the file is deleted

This allows to generate a new forwarding secret simply by deleting the file if required.
The file will only be generated if the forwarding secret is not configured through a system property

resolves #1670

* Add file creation message
2025-10-23 11:13:36 -05:00
Dylan Sperrer f75b512837 Moved pre-1.19.1 command argument validation so it prints the faulty identifier (#1675) 2025-10-21 12:45:04 -05:00
Adrian 7412aca81c Fixed sending ServerData packets if the description component from the backend server is null (#1673) 2025-10-20 19:51:47 -05:00
Adrian 02cf349075 Fixed disconnecting players in the middle of a backend server reconfiguration (#1669) 2025-10-19 09:43:40 -05:00
Andrew Steinborn 67b988e6d2 Update all localizations to use the current year 2025-10-18 18:58:21 -04:00
Andrew Steinborn d2c13c2a4c Provide encode buffer hint 2025-10-18 17:36:39 -04:00
Andrew Steinborn 498a38cf74 Re-enable adaptive allocator
Recent Netty versions have improved the adaptive allocator, and we shouldn't be seeing the OOM issues others were noticing before. Let's re-enable it.

As for the buffer resizing issue, the upstream issue netty/netty#14912 is long fixed. I think we *should* pre-allocate the buffers beforehand much more aggressively, but that has to be future work.
2025-10-18 16:40:54 -04:00
Andrew Steinborn 13a1c93ea6 Bump Netty to 4.2.7.Final 2025-10-18 16:22:32 -04:00
Shane Freeder 38a0a7ed27 use correct string length for newer MC versions (Fixes #1629) (#1668) 2025-10-18 21:22:26 +01:00
Andrew Steinborn 70c3eabdb1 Minor optimizations for MinecraftCompressorAndLengthEncoder and friends
No need to bounce around changing the writer index, we can just set the value directly.

Also pull out the handshake checks into a separate function, to improve inlining.
2025-10-18 16:15:22 -04:00
okx-code 4cd3b68697 Fix players disconnecting when updating boss bars (#1656)
* Fix 1.20.2+ clients disconnecting when updating boss bars

On 1.20.2, the Minecraft client started clearing boss bars after the login packet, which meant that the ProxyServer#showBossbar API would result in the player getting kicked if the boss bar they were previously shown was updated after switching servers.

Therefore, I have added BossBarManager which drops boss bar packets once the client enters the configure phase to ensure that they do not disconnect, and then re-adds the boss bar once the client enters the login phase.

This ensures that clients do not receive boss bar updates for boss bars that they don't exist and causing them to disconnect. I have also taken care to ensure that this logic only applies on 1.20.2 and up, as it is not necessary for older clients.

---------

Co-authored-by: Adrian Gonzales <adriangonzalesval@gmail.com>
2025-10-16 23:12:57 -05:00
Emil 1140fc65ba fix: Enable EMIT_CLICK_URL_HTTPS on component serializers (#1665) 2025-10-14 13:10:57 -05:00
Ross 5753548b44 Fix SimpleCommand suggestion offset (#1664)
* Fix command suggestion offset

* fix length error

* add test

* checkstyle

---------

Co-authored-by: Ross <2086824-trashp@users.noreply.gitlab.com>
2025-10-13 14:41:33 -05:00
Ross 806b386cdb Fix command suggestion offset (#1662) 2025-10-11 21:11:44 -05:00
Cedric d266059abe Update adventure to version 4.25.0 (#1660) 2025-10-10 03:40:10 -05:00
Aaron b1dd26fbc4 1.21.10 (#1658) 2025-10-07 15:40:25 +01:00
Timon Seidel c8c27af7c3 feat: Add primitive support for sound api (#1422)
* feat: Add primitive support for sound api

* change to fail silently

fix: implement the correct playSound method
fix: bumped "since" version

* chore: update 1.21.5

* chore: enforce adventure's policy of not throwing exceptions on unsupported actions

* feat: allow sounds to be played from other players (on the same server)

* chore(fix): add missing getters/setters in packets

* chore: update 1.21.6
chore: added own notes to playSound method, as adventure moved them to the Sound class

* chore: cleanup

* fix: ignore invalid sound source
fix: sound source error on wrong version

* chore: prettify key writing

* Implement missing Player#playSound(Sound)

* Reverted Player#playSound(Sound) implementation

Also, improved documentation related to #playSound mehtods

* chore(jd): mark dialog operations unsupported

* chore: update 1.21.9

---------

Co-authored-by: Adrian Gonzales <adriangonzalesval@gmail.com>
2025-09-29 09:22:19 -05:00
Aaron ba01492790 Minecraft 1.21.9 (#1651)
* 1.21.9-pr1
- not tested yet

* 1.21.9-pre2

* feat: forward code of conduct packets in CONFIG state

* 1.21.9

---------

Co-authored-by: Emilxyz <12966472+Emilxyz@users.noreply.github.com>
2025-09-27 12:15:02 -05:00
Adrian Gonzales 94368d5021 Update publishing endpoint 2025-09-25 19:28:54 -05:00
Joo200 ec793a9fdb Log console command executions (#1137) 2025-09-24 00:50:05 -05:00
DartCZ 37f622f226 feat: add ProxyPreShutdownEvent before players are disconnected (#1626)
* feat: delay player disconnect until ProxyShutdownEvent completes

* fix: added back empty line

* feat: added ProxyPreShutdownEvent

* feat: CR changes

* chore: fixed license, annotated with Beta annotation

* Update proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java

Co-authored-by: Timon Seidel <timong.seidel@gmail.com>

* Update proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java

Co-authored-by: Timon Seidel <timong.seidel@gmail.com>

* chore: consolidated log message

* Update proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java

Co-authored-by: Timon Seidel <timong.seidel@gmail.com>

* Update proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java

Co-authored-by: powercas_gamer <cas@mizule.dev>

* Update api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPreShutdownEvent.java

Co-authored-by: powercas_gamer <cas@mizule.dev>

* feat: make ProxyPreShutdownEvent timeout configurable via system property

* fix: cs

* Document velocity.pre-shutdown-timeout system property

---------

Co-authored-by: Timon Seidel <timong.seidel@gmail.com>
Co-authored-by: powercas_gamer <cas@mizule.dev>
Co-authored-by: Adrian Gonzales <adriangonzalesval@gmail.com>
2025-09-22 22:27:25 +01:00
Ecconia 87f74eaeda [ci skip] Improve documentation for priority parameter in EventManager (#1619)
* Improve documentation for priority parameter in EventManager

PostOrder was deprecated in commit
 (4f227badc2) in favor of priorities.
PostOrder itself was very descriptive on which PostOrder is processed first. A number cannot be descriptive about that - it is never clear if higher or lower numbers are processed first.

The Subscribe event attribute does contain a description on how priorities are evaluated. The EventManager did not, which literally did confused developers manually registering events.

This commit fixes this by describing the priority argument in EventManager with the same description that Subscribe uses.

* Fixed checkstyle

---------

Co-authored-by: Adrian Gonzales <adriangonzalesval@gmail.com>
2025-09-22 14:34:06 -05:00
ᑕᖇEEᑭYᑕᖇEEᑭEᖇ 8406979e71 Fix Weird Behavior in the Annotation Processor (#1645)
* fix: weird behavior of the annotation processor

* optimize imports
2025-09-17 12:17:48 -05:00
VelVeV 6e80f57739 Bump to Netty 4.2.5 (#1643) 2025-09-07 13:05:41 -05:00
Emil 1532fb360b fix: forward custom click packet in config state (#1641) 2025-09-03 22:43:02 +01:00
Emil 180af8c844 fix(resourcepack): apply server-side translations to resource pack prompt (#1611) 2025-09-03 11:09:12 -05:00
Timon Seidel 311e2bc18d fix(temp): pass though custom click action in config state (#1640) 2025-09-03 03:44:52 +01:00
Timon Seidel bfd15e1a81 fix: kick logging ignoring config (#1636) 2025-08-31 10:14:32 -05:00
Shane Freeder d2d333a958 Bumpy netty to 4.2.4 2025-08-14 21:14:51 +01:00
Emil 60a22ff330 chore: bump adventure to 4.24.0 (#1628) 2025-08-14 17:44:06 +01:00
Emil 946e5c47d4 fix: send callback command to >= 1.21.6 clients (#1627) 2025-08-14 17:43:20 +01:00
Gero 5d450ab3c7 Support all component-like and literal tooltips and errors (#1600) 2025-08-13 15:12:15 -05:00
Timon Seidel a509a878e9 [ci skip] chore: migrate legacy url (#1606) 2025-08-13 14:09:52 -05:00
Andrew Steinborn 49e2988e37 Utilize ByteBuf.readString() 2025-08-08 21:47:49 -04:00
Andrew Steinborn db8d16fd6e Bump to Netty 4.2.3
Closes #1615
2025-08-08 21:44:05 -04:00
Emil d47848cb93 feat: map show_dialog & clear_dialog in CONFIG state (#1621) 2025-08-02 17:43:06 +01:00
Chaoscaot 873fca763d Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build successful
2025-07-28 18:34:52 +02:00
Pantera (Mad_Daniel) e99407132f Add version information for 1.21.8 (#1612) 2025-07-18 03:26:24 +01:00
Chaoscaot 67d63faeca Reapply "Disable io_uring transport by default"
SteamWarCI Build successful
This reverts commit 11834de220.
2025-07-10 10:42:11 +02:00
Chaoscaot 871b053561 Merge remote-tracking branch 'upstream/dev/3.0.0' 2025-07-10 10:41:50 +02:00
Shane Freeder 81deb1fff8 Update maven publishing repo name 2025-06-30 15:22:39 +01:00
Jones 59560ebad1 1.21.7 Support (#1598)
* Support 1.21.7 RC 1

* Use snapshot protocol for RC 1

* Support 1.21.7 RC 2

* Set release protocol for 1.21.7

* Update api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java

---------

Co-authored-by: Nassim Jahnke <nassim@njahnke.dev>
2025-06-30 16:09:39 +02:00
Limbo 67a6600c05 New Crowdin updates (#1283)
* New translations messages.properties (Bulgarian)

* New translations messages.properties (Spanish)
2025-06-29 23:31:24 -07:00
Christoph Loy f3e30558e4 Gradle deprecation fixes & upgrades (#1594)
* Fix Gradle deprecations

By using test suites, we explicitely configure the relevant dependencies
on the test sourceset. This is not done by merely configuring the test task.

* Switch to maintained version of Shadow

* Update to Gradle 8.14.2
2025-06-28 16:28:29 -07:00
Riley Park e46ab6ad7d build: publish using fill (#1599) 2025-06-28 16:12:00 -07:00
Gero b6fd48f282 Update to adventure 4.22.0 (#1595) 2025-06-27 16:56:05 +01:00
Chaoscaot c2edc26d8e Merge pull request 'Update Velocity' (#2) from update into master
SteamWarCI Build successful
Reviewed-on: #2
2025-06-26 23:11:02 +02:00
Chaoscaot 76417b13d4 Merge branch 'updatev2' into update
SteamWarCI Build successful
2025-06-26 22:53:32 +02:00
Chaoscaot 91a61643bd Revert "Disable io_uring transport by default"
SteamWarCI Build successful
This reverts commit ae312339a3.
2025-04-27 20:24:41 +02:00
Chaoscaot b6e05cb0b9 Refactor TCP Fast Open checks and update message identifiers.
SteamWarCI Build successful
Removed transport type conditions for TCP Fast Open to streamline configuration usage. Added imports for new message identifiers in `ClientPlaySessionHandler`. Cleaned up Netty library definitions in `libs.versions.toml`.
2025-04-27 20:09:05 +02:00
Chaoscaot 1507b91463 Merge remote-tracking branch 'upstream/dev/3.0.0' into update
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java
2025-04-27 19:53:15 +02:00
Lixfel b06af3718c Merge remote-tracking branch 'github/dev/3.0.0'
SteamWarCI Build successful
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java
2025-04-01 07:06:14 +02:00
Lixfel a20a896582 Skip javadoc generation
SteamWarCI Build successful
2025-01-22 09:37:30 +01:00
Lixfel e1a3421212 Adapt to new server
SteamWarCI Build failed
2025-01-22 09:33:59 +01:00
Lixfel 19e51a2b12 Merge remote-tracking branch 'upstream/dev/3.0.0' 2024-12-06 11:14:27 +01:00
Lixfel b89a5c5ce9 Fix CI 2024-12-02 12:45:04 +01:00
Lixfel 65d3277319 Merge remote-tracking branch 'upstream/dev/3.0.0' 2024-11-30 09:25:25 +01:00
Lixfel a22bfa10f9 Merge pull request 'Update 1.21.2 client support' (#5) from upstream into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Velocity/pulls/5
2024-11-11 08:15:32 +01:00
Lixfel d9d1319a3a Merge remote-tracking branch 'upstream/dev/3.0.0' into upstream
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2024-11-10 18:32:48 +01:00
Lixfel 15ecbf4345 Merge pull request 'Update Velocity (might fix Command problems) and fix PluginMessages...' (#4) from fix-pluginmessages into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Velocity/pulls/4
Reviewed-by: YoyoNow <jwsteam@nidido.de>
2024-08-20 08:14:11 +02:00
Lixfel 5e3bbcd427 Fix command signature issues. 2024-08-20 08:03:18 +02:00
Lixfel a6c79db07b Remove filter checks to receive PluginMessages unfiltered. 2024-08-18 15:32:08 +02:00
Lixfel 6e33bc6c17 Merge remote-tracking branch 'refs/remotes/upstream/dev/3.0.0' 2024-08-18 15:28:10 +02:00
Lixfel 01208bb359 Indicate NoChatReports support in ServerPing 2024-06-24 18:36:32 +02:00
Lixfel fa88aaae52 Always unsign chat. 2024-06-21 12:48:58 +02:00
Lixfel 2da400a267 Merge pull request 'Implement Velocity PRs #998 #1246 and #1309 (io_uring, tcp_fastopen and PluginMessage race condition fix)' (#3) from io_uring into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Velocity/pulls/3
Reviewed-by: YoyoNow <jwsteam@nidido.de>
2024-06-19 12:06:10 +02:00
Lixfel 8103135dfb Fix type 2024-06-19 10:21:36 +02:00
Lixfel cfabff7288 Implement Velocity PRs #998 #1246 and #1309 (io_uring, tcp_fastopen and PluginMessage race condition fix) 2024-06-19 10:11:32 +02:00
Lixfel 2f5a27a708 Fix CI 2024-06-19 09:47:18 +02:00
Lixfel fdfe8bcc4b Fix CI 2024-06-19 09:40:58 +02:00
Chaoscaot a19fd8db74 Add UpdateTeamsPacket 2024-06-16 21:27:13 +02:00
Chaoscaot e63d71423d Add UpdateTeamsPacket 2024-06-16 21:24:06 +02:00
Lixfel a7afe35fab Rebuild 2024-06-16 13:25:07 +02:00
Lixfel 56d6339313 Fix JVM 2024-06-16 13:18:29 +02:00
Lixfel 2475572573 Add steamwarci.yml 2024-06-16 12:52:43 +02:00
179 changed files with 4220 additions and 1603 deletions
+7 -7
View File
@@ -6,18 +6,18 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: gradle/actions/setup-gradle@v5
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: 17
distribution: 'temurin'
java-version: 21
distribution: 'zulu'
- name: Build with Gradle
run: ./gradlew build
+6
View File
@@ -34,3 +34,9 @@ and you can configure it from there.
Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity)
page.
# Localisation
Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity).
If you want to translate a language not available on Crowdin,
you might want to ask in the [Discord](https://discord.gg/papermc) about it.
+3 -3
View File
@@ -59,18 +59,18 @@ tasks {
val o = options as StandardJavadocDocletOptions
o.encoding = "UTF-8"
o.source = "17"
o.source = "21"
o.use()
o.links(
"https://www.slf4j.org/apidocs/",
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
"https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
"https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
"https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
)
o.tags(
@@ -14,7 +14,6 @@ import com.velocitypowered.api.plugin.Plugin;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Objects;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
@@ -68,8 +67,8 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
Name qualifiedName = ((TypeElement) element).getQualifiedName();
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
if (!warnedAboutMultiplePlugins) {
if (pluginClassFound != null) {
if (!pluginClassFound.equals(qualifiedName.toString()) && !warnedAboutMultiplePlugins) {
environment.getMessager()
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
+ "multiple plugins. We are using " + pluginClassFound
@@ -24,7 +24,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/
public final class SerializedPluginDescription {
public static final Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");
public static final String ID_PATTERN_STRING = "[a-z][a-z0-9-_]{0,63}";
public static final Pattern ID_PATTERN = Pattern.compile(ID_PATTERN_STRING);
// @Nullable is used here to make GSON skip these in the serialized file
private final String id;
@@ -23,7 +23,7 @@ public interface CommandSource extends Audience, PermissionSubject {
* Sends a message with the MiniMessage format to this source.
*
* @param message MiniMessage content
* @see <a href="https://docs.advntr.dev/minimessage/format.html">MiniMessage docs</a>
* @see <a href="https://docs.papermc.io/adventure/minimessage/format/">MiniMessage docs</a>
* for more information on the format.
**/
default void sendRichMessage(final @NotNull String message) {
@@ -31,14 +31,14 @@ public interface CommandSource extends Audience, PermissionSubject {
}
/**
* Sends a message with the MiniMessage format to this source.
*
* @param message MiniMessage content
* @param resolvers resolvers to use
* @see <a href="https://docs.advntr.dev/minimessage/">MiniMessage docs</a>
* and <a href="https://docs.advntr.dev/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
* for more information on the format.
**/
* Sends a message with the MiniMessage format to this source.
*
* @param message MiniMessage content
* @param resolvers resolvers to use
* @see <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a>
* and <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
* for more information on the format.
*/
default void sendRichMessage(
final @NotNull String message,
final @NotNull TagResolver @NotNull... resolvers
@@ -60,7 +60,8 @@ public interface EventManager {
*
* @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register
* @param postOrder the relative order in which events should be posted to the handler
* @param postOrder the relative order in which events should be posted to the handler. The higher
* the priority, the earlier the event handler will be called
* @param handler the handler to register
* @param <E> the event type to handle
*/
@@ -11,6 +11,7 @@ import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.event.annotation.AwaitingEvent;
import com.velocitypowered.api.proxy.Player;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* This event is fired once the player has been authenticated, but before they connect to a server.
@@ -22,10 +23,24 @@ import com.velocitypowered.api.proxy.Player;
public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
private final Player player;
private final String serverIdHash;
private ComponentResult result;
@Deprecated(forRemoval = true)
public LoginEvent(Player player) {
this(player, null);
}
/**
* Constructs a new {@link LoginEvent}.
*
* @param player the player who has completed authentication
* @param serverIdHash the server ID hash sent to Mojang for authentication,
* or {@code null} if the connection is in offline-mode
*/
public LoginEvent(Player player, @Nullable String serverIdHash) {
this.player = Preconditions.checkNotNull(player, "player");
this.serverIdHash = serverIdHash;
this.result = ComponentResult.allowed();
}
@@ -33,6 +48,16 @@ public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentRe
return player;
}
/**
* Returns the server ID hash that was sent to Mojang to authenticate the player.
* If the connection was in offline-mode, this returns {@code null}.
*
* @return the server ID hash that was sent to Mojang to authenticate the player
*/
public @Nullable String getServerIdHash() {
return serverIdHash;
}
@Override
public ComponentResult getResult() {
return result;
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
package com.velocitypowered.api.event.player;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import java.util.List;
/**
* This event is fired when a client ({@link Player}) sends a plugin message through the
* unregister channel. Velocity will not wait on this event to finish firing.
*/
public final class PlayerChannelUnregisterEvent {
private final Player player;
private final List<ChannelIdentifier> channels;
public PlayerChannelUnregisterEvent(Player player, List<ChannelIdentifier> channels) {
this.player = Preconditions.checkNotNull(player, "player");
this.channels = Preconditions.checkNotNull(channels, "channels");
}
public Player getPlayer() {
return player;
}
public List<ChannelIdentifier> getChannels() {
return channels;
}
@Override
public String toString() {
return "PlayerChannelUnregisterEvent{"
+ "player=" + player
+ ", channels=" + channels
+ '}';
}
}
@@ -143,7 +143,7 @@ public final class ServerPreConnectEvent implements
* is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status
* {@link Status#CONNECTION_CANCELLED}.
*
* @return a result to deny conneections
* @return a result to deny connections
*/
public static ServerResult denied() {
return DENIED;
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2018-2025 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
package com.velocitypowered.api.event.proxy;
import com.google.common.annotations.Beta;
import com.velocitypowered.api.event.annotation.AwaitingEvent;
/**
* This event is fired by the proxy after it has stopped accepting new connections,
* but before players are disconnected.
* This is the last point at which you can interact with currently connected players,
* for example to transfer them to another proxy or perform other cleanup tasks.
*
* @implNote Velocity will wait for all event listeners to complete before disconnecting players,
* but note that the event will time out after the configured value of the
* <code>velocity.pre-shutdown-timeout</code> system property, default 10 seconds,
* in seconds to prevent shutdown from hanging indefinitely
* @since 3.4.0
*/
@Beta
@AwaitingEvent
public final class ProxyPreShutdownEvent {
@Override
public String toString() {
return "ProxyPreShutdownEvent";
}
}
@@ -91,7 +91,11 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
MINECRAFT_1_21_4(769, "1.21.4"),
MINECRAFT_1_21_5(770, "1.21.5"),
MINECRAFT_1_21_6(771, "1.21.6");
MINECRAFT_1_21_6(771, "1.21.6"),
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"),
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
MINECRAFT_1_21_11(774, "1.21.11"),
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2");
private static final int SNAPSHOT_BIT = 30;
@@ -7,9 +7,11 @@
package com.velocitypowered.api.plugin;
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.intellij.lang.annotations.Pattern;
/**
* Indicates that the {@link Plugin} depends on another plugin in order to enable.
@@ -24,6 +26,7 @@ public @interface Dependency {
* @return The dependency plugin ID
* @see Plugin#id()
*/
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id();
/**
@@ -7,10 +7,12 @@
package com.velocitypowered.api.plugin;
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.intellij.lang.annotations.Pattern;
/**
* Annotation used to describe a Velocity plugin.
@@ -26,6 +28,7 @@ public @interface Plugin {
*
* @return the ID for this plugin
*/
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id();
/**
@@ -29,6 +29,7 @@ import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.function.UnaryOperator;
import net.kyori.adventure.dialog.DialogLike;
import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.key.Key;
@@ -38,6 +39,7 @@ import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
@@ -48,7 +50,8 @@ public interface Player extends
/* Fundamental Velocity interfaces */
CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink,
/* Adventure-specific interfaces */
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable {
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable, Sound.Emitter,
PlayerHeadObjectContents.SkinSource {
/**
* Returns the player's current username.
@@ -335,6 +338,15 @@ public interface Player extends
Component.text(getUsername()))));
}
@SuppressWarnings("UnstableApiUsage") // permitted implementation
@Override
default void applySkinToPlayerHeadContents(
final PlayerHeadObjectContents.@NotNull Builder builder) {
builder.skin(this.getGameProfile());
if (this.hasSentPlayerSettings()) {
builder.hat(this.getPlayerSettings().getSkinParts().hasHat());
}
}
/**
* Gets the player's client brand.
@@ -383,8 +395,12 @@ public interface Player extends
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
*
* @apiNote <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* @see #playSound(Sound, Sound.Emitter)
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void playSound(@NotNull Sound sound) {
@@ -393,8 +409,11 @@ public interface Player extends
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* @apiNote <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* @see #playSound(Sound, Sound.Emitter)
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void playSound(@NotNull Sound sound, double x, double y, double z) {
@@ -403,18 +422,28 @@ public interface Player extends
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* <p><b>Note</b>: Due to <a href="https://bugs.mojang.com/browse/MC/issues/MC-146721">MC-146721</a>, stereo sounds are always played globally in 1.14+.
*
* <p><b>Note</b>: Due to <a href="https://bugs.mojang.com/browse/MC/issues/MC-138832">MC-138832</a>, the volume and pitch are ignored when using this method in 1.14 to 1.16.5.
*
* @param sound the sound to play
* @param emitter the emitter of the sound; may be another player of this player's server
* @since 3.4.0
* @sinceMinecraft 1.19.3
* @apiNote This method is currently only implemented for players on 1.19.3+
* and requires a present {@link #getCurrentServer} for the emitting player as well as this player.
*/
@Override
default void playSound(@NotNull Sound sound, Sound.Emitter emitter) {
default void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
}
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* @param stop the sound and/or a sound source, to stop
* @since 3.4.0
* @sinceMinecraft 1.19.3
* @apiNote This method is currently only implemented for players on 1.19.3+.
*/
@Override
default void stopSound(@NotNull SoundStop stop) {
@@ -425,11 +454,40 @@ public interface Player extends
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void openBook(@NotNull Book book) {
}
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void showDialog(@NotNull DialogLike dialog) {
}
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void closeDialog() {
}
/**
* Transfers a Player to a host.
*
@@ -19,7 +19,9 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.Nullable;
import net.kyori.adventure.text.Component;
import org.jspecify.annotations.Nullable;
/**
* Represents a 1.7 and above server list ping response. This class is immutable.
@@ -28,9 +30,10 @@ public final class ServerPing {
private final Version version;
private final @Nullable Players players;
private final net.kyori.adventure.text.Component description;
private final @Nullable Component description;
private final @Nullable Favicon favicon;
private final @Nullable ModInfo modinfo;
private final boolean preventsChatReports = true;
public ServerPing(Version version, @Nullable Players players,
net.kyori.adventure.text.Component description, @Nullable Favicon favicon) {
@@ -47,8 +50,8 @@ public final class ServerPing {
* @param modinfo the mods this server runs
*/
public ServerPing(Version version, @Nullable Players players,
net.kyori.adventure.text.Component description, @Nullable Favicon favicon,
@Nullable ModInfo modinfo) {
Component description, @Nullable Favicon favicon,
@Nullable ModInfo modinfo) {
this.version = Preconditions.checkNotNull(version, "version");
this.players = players;
this.description = Preconditions.checkNotNull(description, "description");
@@ -64,7 +67,8 @@ public final class ServerPing {
return Optional.ofNullable(players);
}
public net.kyori.adventure.text.Component getDescriptionComponent() {
@Nullable
public Component getDescriptionComponent() {
return description;
}
@@ -151,7 +155,7 @@ public final class ServerPing {
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
private String modType = "FML";
private final List<ModInfo.Mod> mods = new ArrayList<>();
private net.kyori.adventure.text.Component description;
private Component description;
private @Nullable Favicon favicon;
private boolean nullOutPlayers;
private boolean nullOutModinfo;
@@ -299,7 +303,7 @@ public final class ServerPing {
* @param description Component to use as the description.
* @return this builder, for chaining
*/
public Builder description(net.kyori.adventure.text.Component description) {
public Builder description(Component description) {
this.description = Preconditions.checkNotNull(description, "description");
return this;
}
@@ -359,7 +363,7 @@ public final class ServerPing {
return samplePlayers;
}
public Optional<net.kyori.adventure.text.Component> getDescriptionComponent() {
public Optional<Component> getDescriptionComponent() {
return Optional.ofNullable(description);
}
@@ -11,11 +11,14 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
import org.jetbrains.annotations.NotNull;
/**
* Represents a Mojang game profile. This class is immutable.
*/
public final class GameProfile {
public final class GameProfile implements PlayerHeadObjectContents.SkinSource {
private final UUID id;
private final String undashedId;
@@ -169,6 +172,23 @@ public final class GameProfile {
ImmutableList.of());
}
@SuppressWarnings("UnstableApiUsage") // permitted implementation
@Override
public void applySkinToPlayerHeadContents(
final PlayerHeadObjectContents.@NotNull Builder builder) {
if (this.properties.isEmpty()) {
builder.id(this.id);
return;
}
builder.id(this.id)
.name(this.name)
.profileProperties(this.properties.stream()
.map(property -> PlayerHeadObjectContents.property(property.getName(),
property.getValue(), property.getSignature()))
.collect(Collectors.toList()));
}
@Override
public String toString() {
return "GameProfile{"
@@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.withType
import java.io.ByteArrayOutputStream
// This interface is needed as a workaround to get an instance of ExecOperations
interface Injected {
@get:Inject
val execOps: ExecOperations
}
val currentShortRevision = ByteArrayOutputStream().use {
exec {
val execOps = objects.newInstance<Injected>().execOps
execOps.exec {
executable = "git"
args = listOf("rev-parse", "HEAD")
standardOutput = it
@@ -8,10 +8,10 @@ extensions.configure<PublishingExtension> {
maven {
credentials(PasswordCredentials::class.java)
name = "paper"
val base = "https://repo.papermc.io/repository/maven"
val releasesRepoUrl = "$base-releases/"
val snapshotsRepoUrl = "$base-snapshots/"
name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined
val base = "https://artifactory.papermc.io/artifactory"
val releasesRepoUrl = "$base/releases/"
val snapshotsRepoUrl = "$base/snapshots/"
setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
}
}
+6 -6
View File
@@ -12,7 +12,7 @@ subprojects {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
languageVersion.set(JavaLanguageVersion.of(21))
}
}
@@ -20,11 +20,11 @@ subprojects {
testImplementation(rootProject.libs.junit)
}
tasks {
test {
useJUnitPlatform()
reports {
junitXml.required.set(true)
testing.suites.named<JvmTestSuite>("test") {
useJUnitJupiter()
targets.all {
testTask.configure {
reports.junitXml.required = true
}
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered
version=3.4.0-SNAPSHOT
version=3.5.0-SNAPSHOT
+25 -25
View File
@@ -1,26 +1,26 @@
[versions]
configurate3 = "3.7.3"
configurate4 = "4.1.2"
configurate4 = "4.2.0"
flare = "2.0.1"
log4j = "2.24.3"
netty = "4.2.1.Final"
log4j = "2.25.3"
netty = "4.2.10.Final"
[plugins]
indra-publishing = "net.kyori.indra.publishing:2.0.6"
shadow = "io.github.goooler.shadow:8.1.5"
spotless = "com.diffplug.spotless:6.25.0"
fill = "io.papermc.fill.gradle:1.0.10"
shadow = "com.gradleup.shadow:9.3.1"
spotless = "com.diffplug.spotless:8.2.0"
[libraries]
adventure-bom = "net.kyori:adventure-bom:4.21.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.21.0"
adventure-facet = "net.kyori:adventure-platform-facet:4.3.4"
asm = "org.ow2.asm:asm:9.8"
auto-service = "com.google.auto.service:auto-service:1.0.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
adventure-bom = "net.kyori:adventure-bom:4.26.1"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.26.1"
adventure-facet = "net.kyori:adventure-platform-facet:4.4.1"
asm = "org.ow2.asm:asm:9.9.1"
auto-service = "com.google.auto.service:auto-service:1.1.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.0.3"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
checker-qual = "org.checkerframework:checker-qual:3.42.0"
bstats = "org.bstats:bstats-base:3.1.0"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3"
checker-qual = "org.checkerframework:checker-qual:3.53.0"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
@@ -33,21 +33,21 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.15"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-jansi:3.30.2"
jline = "org.jline:jline-terminal-jansi:3.30.6"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:5.10.2"
jspecify = "org.jspecify:jspecify:0.3.0"
junit = "org.junit.jupiter:junit-jupiter:5.14.2"
jspecify = "org.jspecify:jspecify:1.0.0"
kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:25.1-jre"
gson = "com.google.code.gson:gson:2.10.1"
guice = "com.google.inject:guice:6.0.0"
guava = "com.google.guava:guava:33.5.0-jre"
gson = "com.google.code.gson:gson:2.13.2"
guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:2.0.0"
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
mockito = "org.mockito:mockito-core:5.10.0"
mockito = "org.mockito:mockito-core:5.21.0"
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
@@ -55,10 +55,10 @@ netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.6.7"
nightconfig = "com.electronwill.night-config:toml:3.8.3"
slf4j = "org.slf4j:slf4j-api:2.0.17"
snakeyaml = "org.yaml:snakeyaml:1.33"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
snakeyaml = "org.yaml:snakeyaml:2.5"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles]
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+6 -10
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -173,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,18 +200,17 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
@@ -249,4 +245,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
exec "$JAVACMD" "$@"
Vendored
+3 -4
View File
@@ -36,7 +36,7 @@ set APP_HOME=%DIRNAME%
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
@@ -91,4 +90,4 @@ exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:omega
+26 -2
View File
@@ -1,14 +1,16 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import io.papermc.fill.model.BuildChannel
plugins {
application
id("velocity-init-manifest")
alias(libs.plugins.shadow)
alias(libs.plugins.fill)
}
application {
mainClass.set("com.velocitypowered.proxy.Velocity")
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true");
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true")
}
tasks {
@@ -25,6 +27,10 @@ tasks {
}
shadowJar {
filesMatching("META-INF/org/apache/logging/log4j/core/config/plugins/**") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
transform(Log4j2PluginsCacheFileTransformer::class.java)
// Exclude all the collection types we don"t intend to use
@@ -108,10 +114,27 @@ tasks {
}
}
val projectVersion = version as String
fill {
project("velocity")
build {
channel = BuildChannel.STABLE
versionFamily("3.0.0")
version(projectVersion)
downloads {
register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile }
nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" }
}
}
}
}
dependencies {
implementation(project(":velocity-api"))
implementation(project(":velocity-native"))
implementation(project(":velocity-proxy-log4j2-plugin"))
implementation(libs.bundles.log4j)
implementation(libs.kyori.ansi)
@@ -148,4 +171,5 @@ dependencies {
testImplementation(libs.mockito)
annotationProcessor(libs.auto.service)
annotationProcessor(libs.log4j.core)
}
-4
View File
@@ -1,4 +0,0 @@
dependencies {
implementation(libs.bundles.log4j)
annotationProcessor(libs.log4j.core)
}
@@ -47,11 +47,6 @@ public class Velocity {
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir"));
}
// Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator
if (System.getProperty("io.netty.allocator.type") == null) {
System.setProperty("io.netty.allocator.type", "pooled");
}
// Disable the resource leak detector by default as it reduces performance. Allow the user to
// override this if desired.
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {
@@ -24,6 +24,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyPreShutdownEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.network.ProtocolVersion;
@@ -81,7 +82,6 @@ import java.net.http.HttpClient;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -104,8 +104,8 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.translation.MiniMessageTranslationStore;
import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationStore;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bstats.MetricsBase;
@@ -119,7 +119,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/
public class VelocityServer implements ProxyServer, ForwardingAudience {
public static final String VELOCITY_URL = "https://velocitypowered.com";
public static final String VELOCITY_URL = "https://papermc.io/software/velocity";
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
public static final Gson GENERAL_GSON = new GsonBuilder()
@@ -150,6 +150,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
)
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
.create();
private static final int PRE_SHUTDOWN_TIMEOUT =
Integer.getInteger("velocity.pre-shutdown-timeout", 10);
private final ConnectionManager cm;
private final ProxyOptions options;
@@ -216,7 +218,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
ProxyVersion version = getVersion();
PluginDescription description = new VelocityPluginDescription(
"velocity", version.getName(), version.getVersion(), "The Velocity proxy",
VELOCITY_URL, ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
version.getName().equals("Velocity") ? VELOCITY_URL : null,
ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
VelocityPluginContainer container = new VelocityPluginContainer(description);
container.setInstance(VelocityVirtualPlugin.INSTANCE);
return container;
@@ -238,8 +241,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
console.setupStreams();
pluginManager.registerPlugin(this.createVirtualPlugin());
registerTranslations();
// Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds
// dangerous, right? We're well within the realm of factoring such a key...
//
@@ -288,6 +289,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
this.doStartupConfigLoad();
registerTranslations();
for (ServerInfo cliServer : options.getServers()) {
servers.register(cliServer);
}
@@ -338,8 +341,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
}
private void registerTranslations() {
final TranslationStore.StringBased<MessageFormat> translationRegistry =
TranslationStore.messageFormat(Key.key("velocity", "translations"));
final MiniMessageTranslationStore translationRegistry =
MiniMessageTranslationStore.create(Key.key("velocity", "translations"));
translationRegistry.defaultLocale(Locale.US);
try {
ResourceUtils.visitResources(VelocityServer.class, path -> {
@@ -578,6 +581,20 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
// done first to refuse new connections
cm.shutdown();
try {
eventManager.fire(new ProxyPreShutdownEvent())
.toCompletableFuture()
.get(PRE_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
} catch (TimeoutException ignored) {
logger.warn("Your plugins took over {} seconds during pre shutdown.",
PRE_SHUTDOWN_TIMEOUT);
} catch (ExecutionException ee) {
logger.error("Exception in ProxyPreShutdownEvent handler; continuing shutdown.", ee);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
logger.warn("Interrupted while waiting for ProxyPreShutdownEvent; continuing shutdown.");
}
ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
for (ConnectedPlayer player : players) {
player.disconnect(reason);
@@ -817,7 +834,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar;
}
@Override
public boolean isShuttingDown() {
return shutdownInProgress.get();
@@ -53,15 +53,23 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
viewer.getProtocolVersion(),
viewer.translateMessage(this.bar.name())
);
viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
viewer.getBossBarManager().writeUpdate(this, BossBarPacket.createAddPacket(this.id, this.bar, name));
return true;
}
return false;
}
public void createDirect(final ConnectedPlayer viewer) {
final ComponentHolder name = new ComponentHolder(
viewer.getProtocolVersion(),
viewer.translateMessage(this.bar.name())
);
viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
}
public boolean viewerRemove(final ConnectedPlayer viewer) {
if (this.viewers.remove(viewer)) {
viewer.getConnection().write(BossBarPacket.createRemovePacket(this.id, this.bar));
viewer.getBossBarManager().remove(this, BossBarPacket.createRemovePacket(this.id, this.bar));
return true;
}
return false;
@@ -84,7 +92,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
this.bar,
new ComponentHolder(viewer.getProtocolVersion(), translated)
);
viewer.getConnection().write(packet);
viewer.getBossBarManager().writeUpdate(this, packet);
}
}
@@ -96,7 +104,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) {
final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) {
viewer.getConnection().write(packet);
viewer.getBossBarManager().writeUpdate(this, packet);
}
}
@@ -108,7 +116,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) {
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) {
viewer.getConnection().write(packet);
viewer.getBossBarManager().writeUpdate(this, packet);
}
}
@@ -120,7 +128,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) {
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) {
viewer.getConnection().write(packet);
viewer.getBossBarManager().writeUpdate(this, packet);
}
}
@@ -132,7 +140,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) {
final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) {
viewer.getConnection().write(packet);
viewer.getBossBarManager().writeUpdate(this, packet);
}
}
}
@@ -344,7 +344,7 @@ final class SuggestionsProvider<S> {
return 0;
});
}
return potentials.get(0);
return potentials.getFirst();
}
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
}
@@ -35,7 +35,6 @@ import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.command.CommandResult;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.VelocityBrigadierMessage;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.event.command.PostCommandInvocationEvent;
import com.velocitypowered.api.plugin.PluginManager;
@@ -59,6 +58,7 @@ import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor;
import org.checkerframework.checker.lock.qual.GuardedBy;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -140,7 +140,7 @@ public class VelocityCommandManager implements CommandManager {
command + " implements multiple registrable Command subinterfaces: "
+ implementedInterfaces);
} else {
this.internalRegister(commandRegistrars.get(0), command, meta);
this.internalRegister(commandRegistrars.getFirst(), command, meta);
}
}
@@ -242,8 +242,8 @@ public class VelocityCommandManager implements CommandManager {
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
if (isSyntaxError) {
final Message message = e.getRawMessage();
if (message instanceof VelocityBrigadierMessage velocityMessage) {
source.sendMessage(velocityMessage.asComponent().applyFallbackStyle(NamedTextColor.RED));
if (message instanceof ComponentLike componentLike) {
source.sendMessage(componentLike.asComponent().applyFallbackStyle(NamedTextColor.RED));
} else {
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
}
@@ -256,7 +256,7 @@ public class VelocityCommandManager implements CommandManager {
}
} catch (final Throwable e) {
// Ugly, ugly swallowing of everything Throwable, because plugins are naughty.
throw new RuntimeException("Unable to invoke command " + parsed.getReader().getString() + "for " + source, e);
throw new RuntimeException("Unable to invoke command " + parsed.getReader().getString() + " for " + source, e);
} finally {
eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result));
}
@@ -400,4 +400,4 @@ public class VelocityCommandManager implements CommandManager {
return MoreExecutors.directExecutor();
}
}
}
}
@@ -70,33 +70,34 @@ public final class VelocityCommands {
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
}
if (delegate instanceof LiteralCommandNode<CommandSource> lcn) {
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
literalBuilder.executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
literalBuilder.then(wrap(child, registrant));
return switch (delegate) {
case LiteralCommandNode<CommandSource> lcn -> {
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
literalBuilder.executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
literalBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
yield literalBuilder.build();
}
if (delegate.getRedirect() != null) {
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand)
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
case ArgumentCommandNode<CommandSource, ?> node -> {
var argBuilder = node.createBuilder().executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
argBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
yield argBuilder.build();
}
return literalBuilder.build();
} else if (delegate instanceof VelocityArgumentCommandNode<CommandSource, ?> vacn) {
return vacn.withCommand(maybeCommand)
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
} else if (delegate instanceof ArgumentCommandNode) {
var argBuilder = delegate.createBuilder().executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
argBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
return argBuilder.build();
} else {
throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
}
default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
};
}
// Normalization
@@ -133,7 +134,7 @@ public final class VelocityCommands {
if (nodes.isEmpty()) {
throw new IllegalArgumentException("Cannot read alias from empty node list");
}
return nodes.get(0).getNode().getName();
return nodes.getFirst().getNode().getName();
}
public static final String ARGS_NODE_NAME = "arguments";
@@ -118,14 +118,12 @@ public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, St
if (this == o) {
return true;
}
if (!(o instanceof VelocityArgumentCommandNode)) {
if (!(o instanceof VelocityArgumentCommandNode that)) {
return false;
}
if (!super.equals(o)) {
if (!super.equals(that)) {
return false;
}
final VelocityArgumentCommandNode<?, ?> that = (VelocityArgumentCommandNode<?, ?>) o;
return this.type.equals(that.type);
}
@@ -38,6 +38,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/**
* Implements the Velocity default {@code /glist} command.
@@ -111,7 +112,7 @@ public class GlistCommand {
if (registeredServer.isEmpty()) {
source.sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST
.arguments(Component.text(serverName)));
.arguments(Argument.string("server", serverName)));
return -1;
}
sendServerPlayers(source, registeredServer.get(), false);
@@ -126,7 +127,8 @@ public class GlistCommand {
? "velocity.command.glist-player-singular"
: "velocity.command.glist-player-plural"
).color(NamedTextColor.YELLOW)
.arguments(Component.text(Integer.toString(online), NamedTextColor.GREEN));
.arguments(Argument.component(
"players", Component.text(Integer.toString(online), NamedTextColor.GREEN)));
target.sendMessage(msg.build());
}
@@ -35,6 +35,7 @@ import java.util.Objects;
import java.util.Optional;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/**
* Implements the Velocity default {@code /send} command.
@@ -121,7 +122,7 @@ public class SendCommand {
if (maybeServer.isEmpty()) {
context.getSource().sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName))
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Argument.string("server", serverName))
);
return 0;
}
@@ -133,7 +134,7 @@ public class SendCommand {
&& !Objects.equals(player, "all")
&& !Objects.equals(player, "current")) {
context.getSource().sendMessage(
CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player))
CommandMessages.PLAYER_NOT_FOUND.arguments(Argument.string("player", player))
);
return 0;
}
@@ -37,6 +37,7 @@ import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/**
* Implements Velocity's {@code /server} command.
@@ -76,7 +77,7 @@ public final class ServerCommand {
final Optional<RegisteredServer> toConnect = server.getServer(serverName);
if (toConnect.isEmpty()) {
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST
.arguments(Component.text(serverName)));
.arguments(Argument.string("server", serverName)));
return -1;
}
@@ -135,7 +136,7 @@ public final class ServerCommand {
} else {
playersTextComponent.key("velocity.command.server-tooltip-players-online");
}
playersTextComponent.arguments(Component.text(connectedPlayers));
playersTextComponent.arguments(Argument.component("players", Component.text(connectedPlayers)));
if (serverInfo.getName().equals(currentPlayerServer)) {
serverTextComponent.color(NamedTextColor.GREEN)
.hoverEvent(
@@ -62,6 +62,7 @@ import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.translation.Argument;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -165,9 +166,9 @@ public final class VelocityCommand {
.build();
final Component copyright = Component
.translatable("velocity.command.version-copyright",
Component.text(version.getVendor()),
Component.text(version.getName()),
Component.text(LocalDate.now().getYear()));
Argument.string("vendor", version.getVendor()),
Argument.string("name", version.getName()),
Argument.component("year", Component.text(LocalDate.now().getYear())));
source.sendMessage(velocity);
source.sendMessage(copyright);
@@ -176,8 +177,7 @@ public final class VelocityCommand {
.append(Component.text()
.content("PaperMC")
.color(NamedTextColor.GREEN)
.clickEvent(
ClickEvent.openUrl("https://papermc.io/software/velocity"))
.clickEvent(ClickEvent.openUrl(VelocityServer.VELOCITY_URL))
.build())
.append(Component.text(" - "))
.append(Component.text()
@@ -221,7 +221,7 @@ public final class VelocityCommand {
final TranslatableComponent output = Component.translatable()
.key("velocity.command.plugins-list")
.color(NamedTextColor.YELLOW)
.arguments(listBuilder.build())
.arguments(Argument.component("plugins", listBuilder.build()))
.build();
source.sendMessage(output);
return Command.SINGLE_SUCCESS;
@@ -237,17 +237,17 @@ public final class VelocityCommand {
hoverText.append(Component.newline());
hoverText.append(Component.translatable(
"velocity.command.plugin-tooltip-website",
Component.text(url)));
Argument.component("url", Component.text(url))));
});
if (!description.getAuthors().isEmpty()) {
hoverText.append(Component.newline());
if (description.getAuthors().size() == 1) {
hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author",
Component.text(description.getAuthors().get(0))));
Component.text(description.getAuthors().getFirst())));
} else {
hoverText.append(
Component.translatable("velocity.command.plugin-tooltip-author",
Component.text(String.join(", ", description.getAuthors()))
Argument.string("authors", String.join(", ", description.getAuthors()))
)
);
}
@@ -103,13 +103,17 @@ abstract class InvocableCommandRegistrar<T extends InvocableCommand<I>,
.requiresWithContext((context, reader) -> requirement.test(context))
.executes(callback)
.suggests((context, builder) -> {
// Offset the suggestion to the last space seperated word
int lastSpace = builder.getRemaining().lastIndexOf(' ') + 1;
final var offsetBuilder = builder.createOffset(builder.getStart() + lastSpace);
final I invocation = invocationFactory.create(context);
return command.suggestAsync(invocation).thenApply(suggestions -> {
for (String value : suggestions) {
Preconditions.checkNotNull(value, "suggestion");
builder.suggest(value);
offsetBuilder.suggest(value);
}
return builder.build();
return offsetBuilder.build();
});
})
.build();
@@ -29,6 +29,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
import com.velocitypowered.proxy.config.migration.ForwardingMigration;
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil;
@@ -93,6 +94,8 @@ public class VelocityConfiguration implements ProxyConfig {
private @Nullable Favicon favicon;
@Expose
private boolean forceKeyAuthentication = true; // Added in 1.19
@Expose
private PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.DEFAULT;
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
Query query, Metrics metrics) {
@@ -109,7 +112,7 @@ public class VelocityConfiguration implements ProxyConfig {
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
boolean forceKeyAuthentication) {
boolean forceKeyAuthentication, PacketLimiterConfig packetLimiterConfig) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
@@ -128,6 +131,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.query = query;
this.metrics = metrics;
this.forceKeyAuthentication = forceKeyAuthentication;
this.packetLimiterConfig = packetLimiterConfig;
}
/**
@@ -156,19 +160,16 @@ public class VelocityConfiguration implements ProxyConfig {
}
switch (playerInfoForwardingMode) {
case NONE:
logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
case NONE -> logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
+ "from the proxy and will have offline-mode UUIDs.");
break;
case MODERN:
case BUNGEEGUARD:
case MODERN, BUNGEEGUARD -> {
if (forwardingSecret == null || forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set. This is required for security.");
valid = false;
}
break;
default:
break;
}
default -> {
}
}
if (servers.getServers().isEmpty()) {
@@ -449,6 +450,10 @@ public class VelocityConfiguration implements ProxyConfig {
return advanced.isEnableReusePort();
}
public PacketLimiterConfig getPacketLimiterConfig() {
return packetLimiterConfig;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -466,6 +471,7 @@ public class VelocityConfiguration implements ProxyConfig {
.add("favicon", favicon)
.add("enablePlayerAddressLogging", enablePlayerAddressLogging)
.add("forceKeyAuthentication", forceKeyAuthentication)
.add("packetLimiterConfig", packetLimiterConfig)
.toString();
}
@@ -504,6 +510,7 @@ public class VelocityConfiguration implements ProxyConfig {
new ForwardingMigration(),
new KeyAuthenticationMigration(),
new MotdMigration(),
new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration()
};
@@ -515,7 +522,7 @@ public class VelocityConfiguration implements ProxyConfig {
String forwardingSecretString = System.getenv().getOrDefault(
"VELOCITY_FORWARDING_SECRET", "");
if (forwardingSecretString.isEmpty()) {
if (forwardingSecretString.isBlank()) {
final String forwardSecretFile = config.get("forwarding-secret-file");
final Path secretPath = forwardSecretFile == null
? defaultForwardingSecretPath
@@ -528,7 +535,11 @@ public class VelocityConfiguration implements ProxyConfig {
"The file " + forwardSecretFile + " is not a valid file or it is a directory.");
}
} else {
throw new RuntimeException("The forwarding-secret-file does not exist.");
Files.createFile(secretPath);
Files.writeString(secretPath, forwardingSecretString = generateRandomString(12),
StandardCharsets.UTF_8);
logger.info("The forwarding-secret-file does not exist. A new file has been created at {}",
forwardSecretFile);
}
}
final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
@@ -557,6 +568,7 @@ public class VelocityConfiguration implements ProxyConfig {
final boolean kickExisting = config.getOrElse("kick-existing-players", false);
final boolean enablePlayerAddressLogging = config.getOrElse(
"enable-player-address-logging", true);
final PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.fromConfig(config.get("packet-limiter"));
// Throw an exception if the forwarding-secret file is empty and the proxy is using a
// forwarding mode that requires it.
@@ -584,7 +596,8 @@ public class VelocityConfiguration implements ProxyConfig {
new Advanced(advancedConfig),
new Query(queryConfig),
new Metrics(metricsConfig),
forceKeyAuthentication
forceKeyAuthentication,
packetLimiterConfig
);
}
}
@@ -987,4 +1000,33 @@ public class VelocityConfiguration implements ProxyConfig {
return enabled;
}
}
/**
* Configuration for packet limiting.
*
* @param interval the interval in seconds to measure packets over
* @param pps the maximum number of packets per second allowed
* @param bytes the maximum number of bytes per second allowed
*/
public record PacketLimiterConfig(int interval, int pps, int bytes) {
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
/**
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
*
* @param config the configuration object to parse
* @return the packet limiter config, or the default if {@code config} is null
*/
public static PacketLimiterConfig fromConfig(CommentedConfig config) {
if (config != null) {
return new PacketLimiterConfig(
config.getIntOrElse("interval", DEFAULT.interval()),
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
config.getIntOrElse("bytes-per-second", DEFAULT.bytes())
);
} else {
return DEFAULT;
}
}
}
}
@@ -28,6 +28,7 @@ public sealed interface ConfigurationMigration
permits ForwardingMigration,
KeyAuthenticationMigration,
MotdMigration,
MiniMessageTranslationsMigration,
TransferIntegrationMigration {
boolean shouldMigrate(CommentedFileConfig config);
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2024 Velocity 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 com.velocitypowered.proxy.config.migration;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.apache.logging.log4j.Logger;
/**
* Migration from old to modern language argument format with MiniMessage.
* Also migrates possible use of legacy colors to MiniMessage format.
*/
public final class MiniMessageTranslationsMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(final CommentedFileConfig config) {
// Checking whether translations should be migrated would be just as costly as attempting to migrate them directly.
return true;
}
@Override
public void migrate(final CommentedFileConfig config, final Logger logger) throws IOException {
final Path langFolder = Path.of("lang");
if (Files.notExists(langFolder)) {
return;
}
final Pattern oldPlaceholderPattern = Pattern.compile("\\{(\\d+)}");
try (final DirectoryStream<Path> stream
= Files.newDirectoryStream(langFolder, Files::isRegularFile)) {
for (final Path path : stream) {
String content = Files.readString(path, StandardCharsets.UTF_8);
if (content.indexOf('{') == -1) {
continue;
}
// Migrate old arguments
content = oldPlaceholderPattern.matcher(content).replaceAll("<arg:$1>");
// Some setups use legacy color codes, this format is migrated to MiniMessage
content = MiniMessage.miniMessage().serialize(
LegacyComponentSerializer.legacySection().deserialize(content));
Files.writeString(path, content, StandardCharsets.UTF_8);
}
}
}
}
@@ -33,6 +33,7 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
@@ -66,7 +67,7 @@ import io.netty.util.ReferenceCountUtil;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -108,7 +109,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
this.server = server;
this.state = StateRegistry.HANDSHAKE;
this.sessionHandlers = new HashMap<>();
this.sessionHandlers = new EnumMap<>(StateRegistry.class);
}
@Override
@@ -153,13 +154,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (msg instanceof MinecraftPacket pkt) {
if (!pkt.handle(activeSessionHandler)) {
activeSessionHandler.handleGeneric((MinecraftPacket) msg);
activeSessionHandler.handleGeneric(pkt);
}
} else if (msg instanceof HAProxyMessage proxyMessage) {
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) {
activeSessionHandler.handleUnknown((ByteBuf) msg);
} else if (msg instanceof ByteBuf buf) {
activeSessionHandler.handleUnknown(buf);
}
} finally {
ReferenceCountUtil.release(msg);
@@ -368,6 +369,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setState(StateRegistry state) {
ensureInEventLoop();
final StateRegistry previousState = this.state;
this.state = state;
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
.get(MinecraftVarintFrameDecoder.class);
@@ -388,7 +390,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (state == StateRegistry.CONFIG) {
// Activate the play packet queue
addPlayPacketQueueHandler();
if (previousState == StateRegistry.PLAY
&& this.pendingConfigurationSwitch
&& this.association instanceof ConnectedPlayer) {
addPlayPacketQueueOutboundHandler();
} else {
addPlayPacketQueueHandler();
}
} else {
// Remove the queue
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
@@ -404,13 +412,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* Adds the play packet queue handler.
*/
public void addPlayPacketQueueHandler() {
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
}
addPlayPacketQueueOutboundHandler();
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection()));
new PlayPacketQueueInboundHandler(this.protocolVersion,
channel.pipeline().get(MinecraftDecoder.class).getDirection()));
}
}
/**
* Adds only the outbound play packet queue handler.
*/
public void addPlayPacketQueueOutboundHandler() {
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueOutboundHandler(this.protocolVersion,
channel.pipeline().get(MinecraftEncoder.class).getDirection()));
}
}
@@ -544,9 +562,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
} else {
int level = server.getConfiguration().getCompressionLevel();
VelocityCompressor compressor = Natives.compress.get().create(level);
final MinecraftDecoder minecraftDecoder = (MinecraftDecoder) channel.pipeline().get(MINECRAFT_DECODER);
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
decoder = new MinecraftCompressDecoder(threshold, compressor);
decoder = new MinecraftCompressDecoder(threshold, compressor, minecraftDecoder.getDirection());
channel.pipeline().remove(FRAME_ENCODER);
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
@@ -23,7 +23,11 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DialogClearPacket;
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
@@ -48,6 +52,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -67,6 +72,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerComma
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
@@ -364,4 +371,32 @@ public interface MinecraftSessionHandler {
default boolean handle(ClientboundServerLinksPacket packet) {
return false;
}
default boolean handle(DialogClearPacket packet) {
return false;
}
default boolean handle(DialogShowPacket packet) {
return false;
}
default boolean handle(ServerboundCustomClickActionPacket packet) {
return false;
}
default boolean handle(CodeOfConductPacket packet) {
return false;
}
default boolean handle(CodeOfConductAcceptPacket packet) {
return false;
}
default boolean handle(ClientboundSoundEntityPacket packet) {
return false;
}
default boolean handle(ClientboundStopSoundPacket packet) {
return false;
}
}
@@ -34,6 +34,8 @@ import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.command.CommandGraphInjector;
@@ -68,6 +70,7 @@ import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
@@ -91,6 +94,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
Boolean.getBoolean("velocity.log-server-backpressure");
private static final int MAXIMUM_PACKETS_TO_FLUSH =
Integer.getInteger("velocity.max-packets-per-flush", 8192);
private static final int LARGE_PACKET_THRESHOLD = 1024 * 128;
private final VelocityServer server;
private final VelocityServerConnection serverConn;
@@ -177,10 +181,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(BossBarPacket packet) {
if (packet.getAction() == BossBarPacket.ADD) {
playerSessionHandler.getServerBossBars().add(packet.getUuid());
} else if (packet.getAction() == BossBarPacket.REMOVE) {
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
if (serverConn.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
if (packet.getAction() == BossBarPacket.ADD) {
playerSessionHandler.getServerBossBars().add(packet.getUuid());
} else if (packet.getAction() == BossBarPacket.REMOVE) {
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
}
}
return false; // forward
}
@@ -290,31 +296,14 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true;
}
// Register and unregister packets are simply forwarded to the server as-is.
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
return false;
}
if (PluginMessageUtil.isMcBrand(packet)) {
PluginMessagePacket rewritten = PluginMessageUtil
.rewriteMinecraftBrand(packet,
server.getVersion(), playerConnection.getProtocolVersion());
playerConnection.write(rewritten);
return true;
}
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
// Handled.
return true;
}
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
return false;
}
byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy);
String channel = packet.getChannel();
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
PluginMessagePacket copied = new PluginMessagePacket(
@@ -359,7 +348,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
// Inject commands from the proxy.
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
injector.inject(rootNode, serverConn.getPlayer());
rootNode.removeChildByName("velocity:callback");
// In 1.21.6 a confirmation prompt was added when executing a command via `run_command` click
// action if the command is unknown. To prevent this prompt we have to send the command.
if (this.playerConnection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6)) {
rootNode.removeChildByName("velocity:callback");
}
}
server.getEventManager().fire(
@@ -445,11 +439,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void handleGeneric(MinecraftPacket packet) {
if (packet instanceof PluginMessagePacket) {
((PluginMessagePacket) packet).retain();
if (packet instanceof PluginMessagePacket pluginMessage) {
pluginMessage.retain();
}
boolean huge = packet instanceof DeferredByteBufHolder def && def.content().readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(packet);
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush();
packetsFlushed = 0;
}
@@ -457,8 +452,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void handleUnknown(ByteBuf buf) {
boolean huge = buf.readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(buf.retain());
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush();
packetsFlushed = 0;
}
@@ -511,4 +507,4 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
playerConnection.setAutoReading(writable);
}
}
}
@@ -344,66 +344,30 @@ public class BungeeCordMessageResponder {
return false;
}
ByteBufDataInput in = new ByteBufDataInput(message.content());
String subChannel = in.readUTF();
final ByteBufDataInput in = new ByteBufDataInput(message.content());
final String subChannel = in.readUTF();
switch (subChannel) {
case "GetPlayerServer":
this.processGetPlayerServer(in);
break;
case "ForwardToPlayer":
this.processForwardToPlayer(in);
break;
case "Forward":
this.processForwardToServer(in);
break;
case "Connect":
this.processConnect(in);
break;
case "ConnectOther":
this.processConnectOther(in);
break;
case "IP":
this.processIp(in);
break;
case "PlayerCount":
this.processPlayerCount(in);
break;
case "PlayerList":
this.processPlayerList(in);
break;
case "GetServers":
this.processGetServers();
break;
case "Message":
this.processMessage(in);
break;
case "MessageRaw":
this.processMessageRaw(in);
break;
case "GetServer":
this.processGetServer();
break;
case "UUID":
this.processUuid();
break;
case "UUIDOther":
this.processUuidOther(in);
break;
case "IPOther":
this.processIpOther(in);
break;
case "ServerIP":
this.processServerIp(in);
break;
case "KickPlayer":
this.processKick(in);
break;
case "KickPlayerRaw":
this.processKickRaw(in);
break;
default:
// Do nothing, unknown command
break;
case "GetPlayerServer" -> this.processGetPlayerServer(in);
case "ForwardToPlayer" -> this.processForwardToPlayer(in);
case "Forward" -> this.processForwardToServer(in);
case "Connect" -> this.processConnect(in);
case "ConnectOther" -> this.processConnectOther(in);
case "IP" -> this.processIp(in);
case "PlayerCount" -> this.processPlayerCount(in);
case "PlayerList" -> this.processPlayerList(in);
case "GetServers" -> this.processGetServers();
case "Message" -> this.processMessage(in);
case "MessageRaw" -> this.processMessageRaw(in);
case "GetServer" -> this.processGetServer();
case "UUID" -> this.processUuid();
case "UUIDOther" -> this.processUuidOther(in);
case "IPOther" -> this.processIpOther(in);
case "ServerIP" -> this.processServerIp(in);
case "KickPlayer" -> this.processKick(in);
case "KickPlayerRaw" -> this.processKickRaw(in);
default -> {
// Do nothing, unknown command
}
}
return true;
@@ -52,6 +52,7 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
@@ -59,7 +60,7 @@ import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import java.io.IOException;
import io.netty.channel.Channel;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.key.Key;
@@ -71,6 +72,9 @@ import org.apache.logging.log4j.Logger;
* 1.20.2+ switching. Yes, some of this is exceptionally stupid.
*/
public class ConfigSessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class);
private final VelocityServer server;
private final VelocityServerConnection serverConn;
@@ -256,7 +260,13 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(DisconnectPacket packet) {
serverConn.disconnect();
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
// If the player receives a DisconnectPacket without a connection to a server in progress,
// it means that the backend server has kicked the player during reconfiguration
if (serverConn.getPlayer().getConnectionInFlight() != null) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
} else {
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), packet, true);
}
return true;
}
@@ -358,10 +368,16 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
return true;
}
@Override
public boolean handle(CodeOfConductPacket packet) {
this.serverConn.getPlayer().getConnection().write(packet.retain());
return true;
}
@Override
public void disconnected() {
resultFuture.completeExceptionally(
new IOException("Unexpectedly disconnected from remote server"));
resultFuture.complete(ConnectionRequestResults.forDisconnect(
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
}
@Override
@@ -369,6 +385,22 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
serverConn.getPlayer().getConnection().write(packet);
}
@Override
public void writabilityChanged() {
Channel serverChan = serverConn.ensureConnected().getChannel();
boolean writable = serverChan.isWritable();
if (BACKPRESSURE_LOG) {
if (writable) {
logger.info("{} is writable, will auto-read player connection data", this.serverConn);
} else {
logger.info("{} is not writable, not auto-reading player connection data", this.serverConn);
}
}
serverConn.getPlayer().getConnection().setAutoReading(writable);
}
private void switchFailure(Throwable cause) {
logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(),
serverConn.getPlayer().getUsername(), cause);
@@ -382,4 +414,4 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
public enum State {
START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE
}
}
}
@@ -165,7 +165,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
smc.setAutoReading(false);
clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
clientPlaySessionHandler.doSwitch().thenRunAsync(() -> smc.setAutoReading(true), smc.eventLoop());
} else {
// Initial login - the player is already in configuration state.
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
@@ -38,7 +38,6 @@ import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -122,9 +121,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// Change the client to use the ClientPlaySessionHandler if required.
ClientPlaySessionHandler playHandler;
if (player.getConnection()
.getActiveSessionHandler() instanceof ClientPlaySessionHandler) {
playHandler =
(ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler();
.getActiveSessionHandler() instanceof ClientPlaySessionHandler sessionHandler) {
playHandler = sessionHandler;
} else {
playHandler = new ClientPlaySessionHandler(server, player);
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler);
@@ -214,7 +212,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override
public void disconnected() {
resultFuture
.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
resultFuture.complete(ConnectionRequestResults.forDisconnect(
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
}
}
@@ -53,6 +53,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
@@ -70,6 +71,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>();
private @MonotonicNonNull Integer entityId;
/**
* Initializes a new server connection.
@@ -178,9 +180,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setServerAddress(createBungeeGuardForwardingAddress(secret));
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN);
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType) {
handshake.setServerAddress(playerVhost + ((ModernForgeConnectionType) proxyPlayer
.getConnection().getType()).getModernToken());
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType forgeConnection) {
handshake.setServerAddress(playerVhost + forgeConnection.getModernToken());
} else {
handshake.setServerAddress(playerVhost);
}
@@ -324,6 +325,14 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
return pendingPings;
}
public Integer getEntityId() {
return entityId;
}
public void setEntityId(Integer entityId) {
this.entityId = entityId;
}
/**
* Ensures that this server connection remains "active": the connection is established and not
* closed, the player is still connected to the server, and the player still remains online.
@@ -69,14 +69,16 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
private final boolean onlineMode;
private State loginState = State.START; // 1.20.2+
private final String serverIdHash;
AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound,
GameProfile profile, boolean onlineMode) {
GameProfile profile, boolean onlineMode, String serverIdHash) {
this.server = Preconditions.checkNotNull(server, "server");
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.profile = Preconditions.checkNotNull(profile, "profile");
this.onlineMode = onlineMode;
this.mcConnection = inbound.delegatedConnection();
this.serverIdHash = serverIdHash;
}
@Override
@@ -213,7 +215,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) {
mcConnection.setAssociation(player);
server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> {
server.getEventManager().fire(new LoginEvent(player, serverIdHash)).thenAcceptAsync(event -> {
if (mcConnection.isClosed()) {
// The player was disconnected
server.getEventManager().fireAndForget(new DisconnectEvent(player,
@@ -33,6 +33,7 @@ import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResp
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
@@ -40,6 +41,8 @@ import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
@@ -58,6 +61,8 @@ import org.apache.logging.log4j.Logger;
* Handles the client config stage.
*/
public class ClientConfigSessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class);
private final VelocityServer server;
@@ -205,6 +210,26 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
return true;
}
@Override
public boolean handle(ServerboundCustomClickActionPacket packet) {
if (player.getConnectionInFlight() != null) {
player.getConnectionInFlight().ensureConnected().write(packet.retain());
return true;
}
return false;
}
@Override
public boolean handle(CodeOfConductAcceptPacket packet) {
if (this.player.getConnectionInFlight() != null) {
this.player.getConnectionInFlight().ensureConnected().write(packet);
return true;
}
return false;
}
@Override
public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
@@ -244,6 +269,36 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override
public void exception(Throwable throwable) {
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
if (MinecraftDecoder.DEBUG) {
logger.info("Exception while handling packet for {}", player, throwable);
}
}
@Override
public void writabilityChanged() {
final boolean writable = player.getConnection().getChannel().isWritable();
if (BACKPRESSURE_LOG) {
if (writable) {
logger.info("{} is writable, will auto-read backend connection data", player);
} else {
logger.info("{} is not writable, not auto-reading backend connection data", player);
}
}
if (!writable) {
// Flush pending packets to free up memory. Schedule on a future event loop invocation
// to avoid disabling auto-read while the flush resolves backpressure.
player.getConnection().eventLoop().execute(() -> player.getConnection().flush());
}
final VelocityServerConnection serverConn = player.getConnectionInFlightOrConnectedServer();
if (serverConn != null) {
final MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.setAutoReading(writable);
}
}
}
/**
@@ -21,15 +21,17 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.suggestion.Suggestion;
import com.velocitypowered.api.command.VelocityBrigadierMessage;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.ConnectionTypes;
import com.velocitypowered.proxy.connection.MinecraftConnection;
@@ -41,6 +43,7 @@ import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
@@ -87,6 +90,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -97,6 +101,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* center that joins backend servers with players.
*/
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
@@ -318,8 +324,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain());
} else if (PluginMessageUtil.isUnregister(packet)) {
player.getClientsideChannels()
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()));
List<ChannelIdentifier> channels =
PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion());
player.getClientsideChannels().removeAll(channels);
server.getEventManager()
.fireAndForget(
new PlayerChannelUnregisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain());
} else if (PluginMessageUtil.isMcBrand(packet)) {
String brand = PluginMessageUtil.readBrandMessage(packet.content());
@@ -339,43 +349,25 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
if (!player.getPhase().handle(player, packet, serverConn)) {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
// We don't have any plugins listening on this channel, process the packet now.
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// The client is trying to send messages too early. This is primarily caused by mods,
// but further aggravated by Velocity. To work around these issues, we will queue any
// non-FML handshake messages to be sent once the FML handshake has completed or the
// JoinGame packet has been received by the proxy, whichever comes first.
//
// We also need to make sure to retain these packets, so they can be flushed
// appropriately.
loginPluginMessages.add(packet.retain());
} else {
// The connection is ready, send the packet now.
backendConn.write(packet.retain());
}
} else {
byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
Unpooled.wrappedBuffer(copy));
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// We're still processing the connection (see above), enqueue the packet for now.
loginPluginMessages.add(message.retain());
} else {
backendConn.write(message);
}
byte[] copy = ByteBufUtil.getBytes(packet.content());
String channel = packet.getChannel();
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
Unpooled.wrappedBuffer(copy));
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// We're still processing the connection (see above), enqueue the packet for now.
loginPluginMessages.add(message.retain());
} else {
backendConn.write(message);
}
}, backendConn.eventLoop()).exceptionally((ex) -> {
logger.error("Exception while handling plugin message packet for {}", player, ex);
return null;
});
}
}
}, backendConn.eventLoop()).exceptionally((ex) -> {
logger.error("Exception while handling plugin message packet for {}", player, ex);
return null;
});
}
}
}
@@ -463,7 +455,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) {
final boolean stateAllowsForward = smc != null
&& !smc.isClosed()
&& serverConnection.getPhase().consideredComplete()
&& smc.getState() == StateRegistry.PLAY;
if (stateAllowsForward) {
if (packet instanceof PluginMessagePacket) {
((PluginMessagePacket) packet).retain();
}
@@ -480,7 +476,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) {
final boolean stateAllowsForward = smc != null
&& !smc.isClosed()
&& serverConnection.getPhase().consideredComplete()
&& smc.getState() == StateRegistry.PLAY;
if (stateAllowsForward) {
smc.write(buf.retain());
}
}
@@ -492,14 +492,24 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void exception(Throwable throwable) {
player.disconnect(
Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
if (MinecraftDecoder.DEBUG) {
logger.info("Exception while handling packet for {}", player, throwable);
}
}
@Override
public void writabilityChanged() {
boolean writable = player.getConnection().getChannel().isWritable();
if (BACKPRESSURE_LOG) {
if (writable) {
logger.info("{} is writable, will auto-read backend connection data", player);
} else {
logger.info("{} is not writable, not auto-reading backend connection data", player);
}
}
if (!writable) {
// We might have packets queued from the server, so flush them now to free up memory. Make
// sure to do it on a future invocation of the event loop, otherwise while the issue will
@@ -535,9 +545,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Config state clears everything in the client. No need to clear later.
spawned = false;
serverBossBars.clear();
player.clearPlayerListHeaderAndFooterSilent();
player.getTabList().clearAllSilent();
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
player.getBossBarManager().dropPackets();
} else {
serverBossBars.clear();
}
}
player.switchToConfigState();
@@ -575,15 +589,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
}
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
// track them.
for (UUID serverBossBar : serverBossBars) {
BossBarPacket deletePacket = new BossBarPacket();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBarPacket.REMOVE);
player.getConnection().delayedWrite(deletePacket);
destination.setEntityId(joinGame.getEntityId()); // used for sound api
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
player.getBossBarManager().sendBossBars();
} else {
// Remove previous boss bars. These don't get cleared when sending JoinGame (up until 1.20.2),
// thus the need to track them.
for (UUID serverBossBar : serverBossBars) {
BossBarPacket deletePacket = new BossBarPacket();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBarPacket.REMOVE);
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
}
serverBossBars.clear();
// Tell the server about the proxy's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
@@ -694,23 +713,35 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return;
}
List<Offer> offers = new ArrayList<>();
for (Suggestion suggestion : suggestions.getList()) {
String offer = suggestion.getText();
ComponentHolder tooltip = null;
if (suggestion.getTooltip() != null
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
tooltip = new ComponentHolder(player.getProtocolVersion(),
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
int startPos = -1;
for (var suggestion : suggestions.getList()) {
if (startPos == -1 || startPos > suggestion.getRange().getStart()) {
startPos = suggestion.getRange().getStart();
}
offers.add(new Offer(offer, tooltip));
}
int startPos = packet.getCommand().lastIndexOf(' ') + 1;
if (startPos > 0) {
List<Offer> offers = new ArrayList<>();
for (Suggestion suggestion : suggestions.getList()) {
String offer;
if (suggestion.getRange().getStart() == startPos) {
offer = suggestion.getText();
} else {
offer = command.substring(startPos, suggestion.getRange().getStart()) + suggestion.getText();
}
ComponentHolder tooltip = null;
if (suggestion.getTooltip() instanceof ComponentLike componentLike) {
tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent());
} else if (suggestion.getTooltip() != null) {
tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString()));
}
offers.add(new Offer(offer, tooltip));
}
TabCompleteResponsePacket resp = new TabCompleteResponsePacket();
resp.setTransactionId(packet.getTransactionId());
resp.setStart(startPos);
resp.setLength(packet.getCommand().length() - startPos);
resp.setStart(startPos + 1);
resp.setLength(packet.getCommand().length() - startPos - 1);
resp.getOffers().addAll(offers);
player.getConnection().write(resp);
}
@@ -765,10 +796,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
offer = offer.substring(command.length());
}
ComponentHolder tooltip = null;
if (suggestion.getTooltip() != null
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
tooltip = new ComponentHolder(player.getProtocolVersion(),
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
if (suggestion.getTooltip() instanceof ComponentLike componentLike) {
tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent());
} else if (suggestion.getTooltip() != null) {
tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString()));
}
response.getOffers().add(new Offer(offer, tooltip));
}
@@ -62,6 +62,7 @@ import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.player.bossbar.BossBarManager;
import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
@@ -73,6 +74,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
@@ -127,9 +130,12 @@ import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.resource.ResourcePackInfoLike;
import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.minimessage.translation.Argument;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title.Times;
@@ -197,6 +203,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private @Nullable ClientSettingsPacket clientSettingsPacket;
private volatile ChatQueue chatQueue;
private final ChatBuilderFactory chatBuilderFactory;
private final BossBarManager bossBarManager;
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
@@ -223,6 +230,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.chatQueue = new ChatQueue(this);
this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion());
this.resourcePackHandler = ResourcePackHandler.create(this, server);
this.bossBarManager = new BossBarManager(this);
}
/**
@@ -706,12 +714,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component friendlyError;
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
friendlyError = Component.translatable("velocity.error.connected-server-error",
Component.text(server.getServerInfo().getName()));
Argument.string("server", server.getServerInfo().getName()));
} else {
logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(),
wrapped);
friendlyError = Component.translatable("velocity.error.connecting-server-error",
Component.text(server.getServerInfo().getName()));
Argument.string("server", server.getServerInfo().getName()));
}
handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
}
@@ -733,18 +741,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component disconnectReason = disconnect.getReason().getComponent();
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
plainTextReason);
if (this.server.getConfiguration().isLogPlayerConnections()) {
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
plainTextReason);
}
handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED,
Component.text(server.getServerInfo().getName()),
Argument.string("server", server.getServerInfo().getName()),
disconnectReason), safe);
} else {
logger.error("{}: disconnected while connecting to {}: {}", this,
server.getServerInfo().getName(), plainTextReason);
if (this.server.getConfiguration().isLogPlayerConnections()) {
logger.error("{}: disconnected while connecting to {}: {}", this,
server.getServerInfo().getName(), plainTextReason);
}
handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
Component.text(server.getServerInfo().getName()),
Argument.string("server", server.getServerInfo().getName()),
disconnectReason), safe);
}
}
@@ -800,63 +812,56 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return;
}
if (event.getResult() instanceof final DisconnectPlayer res) {
disconnect(res.getReasonComponent());
} else if (event.getResult() instanceof final RedirectPlayer res) {
createConnectionRequest(res.getServer(), previousConnection).connect()
.whenCompleteAsync((status, throwable) -> {
if (throwable != null) {
handleConnectionException(
status != null ? status.getAttemptedConnection() : res.getServer(), throwable,
true);
return;
}
switch (event.getResult()) {
case DisconnectPlayer res -> disconnect(res.getReasonComponent());
case RedirectPlayer res -> createConnectionRequest(res.getServer(), previousConnection).connect()
.whenCompleteAsync((status, throwable) -> {
if (throwable != null) {
handleConnectionException(res.getServer(), throwable, true);
return;
}
switch (status.getStatus()) {
// Impossible/nonsensical cases
case ALREADY_CONNECTED:
logger.error("{}: already connected to {}", this,
status.getAttemptedConnection().getServerInfo().getName());
break;
case CONNECTION_IN_PROGRESS:
// Fatal case
case CONNECTION_CANCELLED:
Component fallbackMsg = res.getMessageComponent();
if (fallbackMsg == null) {
fallbackMsg = friendlyReason;
switch (status.getStatus()) {
// Impossible/nonsensical cases
case ALREADY_CONNECTED -> logger.error("{}: already connected to {}", this,
status.getAttemptedConnection().getServerInfo().getName());
case CONNECTION_IN_PROGRESS, CONNECTION_CANCELLED -> {
Component fallbackMsg = res.getMessageComponent();
if (fallbackMsg == null) {
fallbackMsg = friendlyReason;
}
disconnect(status.getReasonComponent().orElse(fallbackMsg));
}
case SERVER_DISCONNECTED -> {
Component reason = status.getReasonComponent()
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
handleConnectionException(res.getServer(),
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
((Impl) status).isSafe());
}
case SUCCESS -> {
Component requestedMessage = res.getMessageComponent();
if (requestedMessage == null) {
requestedMessage = friendlyReason;
}
if (requestedMessage != Component.empty()) {
sendMessage(requestedMessage);
}
}
default -> {
// The only remaining value is successful (no need to do anything!)
}
}
disconnect(status.getReasonComponent().orElse(fallbackMsg));
break;
case SERVER_DISCONNECTED:
Component reason = status.getReasonComponent()
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
handleConnectionException(res.getServer(),
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
((Impl) status).isSafe());
break;
case SUCCESS:
Component requestedMessage = res.getMessageComponent();
if (requestedMessage == null) {
requestedMessage = friendlyReason;
}
if (requestedMessage != Component.empty()) {
sendMessage(requestedMessage);
}
break;
default:
// The only remaining value is successful (no need to do anything!)
break;
}
}, connection.eventLoop());
} else if (event.getResult() instanceof final Notify res) {
if (event.kickedDuringServerConnect() && previousConnection != null) {
sendMessage(res.getMessageComponent());
} else {
disconnect(res.getMessageComponent());
}, connection.eventLoop());
case Notify res -> {
if (event.kickedDuringServerConnect() && previousConnection != null) {
sendMessage(res.getMessageComponent());
} else {
disconnect(res.getMessageComponent());
}
}
} else {
// In case someone gets creative, assume we want to disconnect the player.
disconnect(friendlyReason);
default -> disconnect(friendlyReason);
}
}, connection.eventLoop());
}
@@ -1038,6 +1043,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.clientBrand = clientBrand;
}
@Override
public void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
Preconditions.checkNotNull(sound, "sound");
Preconditions.checkNotNull(emitter, "emitter");
VelocityServerConnection soundTargetServerConn = getConnectedServer();
if (getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)
|| connection.getState() != StateRegistry.PLAY
|| soundTargetServerConn == null
|| (sound.source() == Sound.Source.UI
&& getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_5))) {
return;
}
VelocityServerConnection soundEmitterServerConn;
if (emitter == Sound.Emitter.self()) {
soundEmitterServerConn = soundTargetServerConn;
} else if (emitter instanceof ConnectedPlayer player) {
if ((soundEmitterServerConn = player.getConnectedServer()) == null) {
return;
}
if (!soundEmitterServerConn.getServer().equals(soundTargetServerConn.getServer())) {
return;
}
} else {
return;
}
connection.write(new ClientboundSoundEntityPacket(sound, null, soundEmitterServerConn.getEntityId()));
}
@Override
public void stopSound(@NotNull SoundStop stop) {
Preconditions.checkNotNull(stop, "stop");
if (getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)
|| connection.getState() != StateRegistry.PLAY
|| (stop.source() == Sound.Source.UI
&& getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_5))) {
return;
}
connection.write(new ClientboundStopSoundPacket(stop));
}
@Override
public void transferToHost(final InetSocketAddress address) {
Preconditions.checkNotNull(address);
@@ -1300,11 +1349,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
if (sentTime != null) {
final MinecraftConnection smc = serverConnection.getConnection();
if (smc != null) {
final StateRegistry clientState = connection.getState();
final boolean stateAllowsForward = smc != null
&& !smc.isClosed()
&& clientState == smc.getState()
&& (clientState == StateRegistry.CONFIG || clientState == StateRegistry.PLAY);
if (stateAllowsForward) {
setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
smc.write(packet);
return true;
}
// We removed this, and so this is ours
return true;
}
}
return false;
@@ -1314,7 +1369,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
* Switches the connection to the client into config state.
*/
public void switchToConfigState() {
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
final VelocityServerConnection targetServer = getConnectionInFlightOrConnectedServer();
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, targetServer))
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
// if the connection was closed earlier, there is a risk that the player is no longer connected
if (!connection.getChannel().isActive()) {
@@ -1329,7 +1385,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
connection.pendingConfigurationSwitch = true;
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
// Make sure we don't send any play packets to the player after update start
connection.addPlayPacketQueueHandler();
connection.addPlayPacketQueueOutboundHandler();
}, connection.eventLoop()).exceptionally((ex) -> {
logger.error("Error switching player connection to config state", ex);
return null;
@@ -1379,6 +1435,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return handshakeIntent;
}
public BossBarManager getBossBarManager() {
return bossBarManager;
}
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect;
@@ -1438,7 +1498,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
VelocityServerConnection con =
new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server);
connectionInFlight = con;
return con.connect().whenCompleteAsync((result, exception) -> this.resetIfInFlightIs(con),
return con.connect().whenCompleteAsync((result, exception) -> {
if (result != null && !result.isSuccessful() && !result.isSafe()) {
handleConnectionException(result.getAttemptedConnection(),
// The only way for the reason to be null is if the result is safe
DisconnectPacket.create(result.getReasonComponent().orElseThrow(),
getProtocolVersion(), connection.getState()), false);
}
this.resetIfInFlightIs(con);
},
connection.eventLoop());
}, connection.eventLoop());
});
@@ -1452,22 +1521,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
@Override
public CompletableFuture<Result> connect() {
return this.internalConnect().whenCompleteAsync((status, throwable) -> {
if (status != null && !status.isSuccessful()) {
if (!status.isSafe()) {
handleConnectionException(status.getAttemptedConnection(), throwable, false);
}
}
}, connection.eventLoop()).thenApply(x -> x);
return this.internalConnect().thenApply(x -> x);
}
@Override
public CompletableFuture<Boolean> connectWithIndication() {
return internalConnect().whenCompleteAsync((status, throwable) -> {
if (throwable != null) {
// TODO: The exception handling from this is not very good. Find a better way.
handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect,
throwable, true);
handleConnectionException(toConnect, throwable, true);
return;
}
@@ -127,10 +127,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
if (!handshake.getProtocolVersion().isSupported()) {
// Bump connection into correct protocol state so that we can send the disconnect packet.
connection.setState(StateRegistry.LOGIN);
ic.disconnectQuietly(Component.translatable()
.key("multiplayer.disconnect.outdated_client")
.arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING))
.build());
ic.disconnectQuietly(Component.translatable(
"multiplayer.disconnect.outdated_client",
Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)
));
return;
}
@@ -152,7 +152,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} else {
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound,
GameProfile.forOfflinePlayer(login.getUsername()), false));
GameProfile.forOfflinePlayer(login.getUsername()), false, null));
}
});
});
@@ -214,6 +214,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
server.getVersion().getName() + "/" + server.getVersion().getVersion())
.uri(URI.create(url))
.build();
//noinspection resource
final HttpClient httpClient = server.createHttpClient();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.whenCompleteAsync((response, throwable) -> {
@@ -254,7 +255,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
}
// All went well, initialize the session.
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, profile, true));
new AuthSessionHandler(server, inbound, profile, true, serverId));
} else if (response.statusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(
@@ -268,14 +269,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
}
}, mcConnection.eventLoop())
.thenRun(() -> {
if (httpClient instanceof final AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
// In Java 21, the HttpClient does not throw any Exception
// when trying to clean its resources, so this should not happen
logger.error("An unknown error occurred while trying to close an HttpClient", e);
}
try {
httpClient.close();
} catch (Exception e) {
// In Java 21, the HttpClient does not throw any Exception
// when trying to clean its resources, so this should not happen
logger.error("An unknown error occurred while trying to close an HttpClient", e);
}
});
} catch (GeneralSecurityException e) {
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2019-2023 Velocity 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 com.velocitypowered.proxy.connection.player.bossbar;
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import java.util.HashSet;
import java.util.Set;
/**
* Handles dropping and resending boss bar packets on versions 1.20.2 and newer because the client now
* deletes all boss bars during the login phase, and sending update packets would cause the client to be disconnected.
*/
public final class BossBarManager {
private final ConnectedPlayer player;
private final Set<VelocityBossBarImplementation> bossBars = new HashSet<>();
private boolean dropPackets = false;
public BossBarManager(ConnectedPlayer player) {
this.player = player;
}
/**
* Records the specified boss bar to be re-sent when a player changes server, and sends the update packet
* if the client is able to receive it and not be disconnected.
*/
public synchronized void writeUpdate(VelocityBossBarImplementation bar, BossBarPacket packet) {
this.bossBars.add(bar);
if (!this.dropPackets) {
this.player.getConnection().write(packet);
}
}
/**
* Removes the specified boss bar from the player to ensure it is not re-sent.
*/
public synchronized void remove(VelocityBossBarImplementation bar, BossBarPacket packet) {
this.bossBars.remove(bar);
if (!this.dropPackets) {
this.player.getConnection().write(packet);
}
}
/**
* Re-creates the boss bars the player can see with any updates that may have occurred in the meantime,
* and allows update packets for those boss bars to be sent.
*/
public synchronized void sendBossBars() {
for (VelocityBossBarImplementation bossBar : bossBars) {
bossBar.createDirect(player);
}
this.dropPackets = false;
}
/**
* Prevents the player from receiving boss bar update packets while logging in to a new server.
*/
public synchronized void dropPackets() {
this.dropPackets = true;
}
}
@@ -94,7 +94,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
this.outstandingResourcePacks.get(info.getId());
outstandingResourcePacks.add(info);
if (outstandingResourcePacks.size() == 1) {
tickResourcePackQueue(outstandingResourcePacks.get(0).getId());
tickResourcePackQueue(outstandingResourcePacks.getFirst().getId());
}
}
@@ -111,7 +111,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
final List<ResourcePackInfo> outstandingResourcePacks =
this.outstandingResourcePacks.get(uuid);
if (!outstandingResourcePacks.isEmpty()) {
sendResourcePackRequestPacket(outstandingResourcePacks.get(0));
sendResourcePackRequestPacket(outstandingResourcePacks.getFirst());
}
}
@@ -124,7 +124,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
this.outstandingResourcePacks.get(uuid);
final boolean peek = bundle.status().isIntermediate();
final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null :
peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0);
peek ? outstandingResourcePacks.getFirst() : outstandingResourcePacks.removeFirst();
server.getEventManager()
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
@@ -111,7 +111,7 @@ public abstract sealed class ResourcePackHandler
}
request.setRequired(queued.getShouldForce());
request.setPrompt(queued.getPrompt() == null ? null :
new ComponentHolder(player.getProtocolVersion(), queued.getPrompt()));
new ComponentHolder(player.getProtocolVersion(), player.translateMessage(queued.getPrompt())));
player.getConnection().write(request);
}
@@ -36,6 +36,7 @@ import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
/**
* Common utilities for handling server list ping results.
@@ -99,58 +100,60 @@ public class ServerListPingHandler {
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback);
switch (mode) {
case ALL:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
return response;
return switch (mode) {
case ALL -> pingResponses.thenApply(responses -> {
// Find the first non-fallback
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
return fallback;
});
case MODS:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback that contains a mod list
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
Optional<ModInfo> modInfo = response.getModinfo();
if (modInfo.isPresent()) {
return fallback.asBuilder().mods(modInfo.get()).build();
}
}
return fallback;
});
case DESCRIPTION:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback. If it includes a modlist, add it too.
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
if (response.getDescriptionComponent() == null) {
continue;
}
return new ServerPing(
fallback.getVersion(),
fallback.getPlayers().orElse(null),
response.getDescriptionComponent(),
fallback.getFavicon().orElse(null),
response.getModinfo().orElse(null)
);
if (response.getDescriptionComponent() == null) {
return response.asBuilder()
.description(Component.empty())
.build();
}
return fallback;
});
return response;
}
return fallback;
});
case MODS -> pingResponses.thenApply(responses -> {
// Find the first non-fallback that contains a mod list
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
Optional<ModInfo> modInfo = response.getModinfo();
if (modInfo.isPresent()) {
return fallback.asBuilder().mods(modInfo.get()).build();
}
}
return fallback;
});
case DESCRIPTION -> pingResponses.thenApply(responses -> {
// Find the first non-fallback. If it includes a modlist, add it too.
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
if (response.getDescriptionComponent() == null) {
continue;
}
return new ServerPing(
fallback.getVersion(),
fallback.getPlayers().orElse(null),
response.getDescriptionComponent(),
fallback.getFavicon().orElse(null),
response.getModinfo().orElse(null)
);
}
return fallback;
});
// Not possible, but covered for completeness.
default:
return CompletableFuture.completedFuture(fallback);
}
default -> CompletableFuture.completedFuture(fallback);
};
}
/**
@@ -33,6 +33,7 @@ import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
@@ -59,11 +60,11 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
private final VelocityServer server;
private PermissionFunction permissionFunction = ALWAYS_TRUE;
private final @NotNull Pointers pointers = ConsoleCommandSource.super.pointers().toBuilder()
.withDynamic(PermissionChecker.POINTER, this::getPermissionChecker)
.withDynamic(Identity.LOCALE, () -> ClosestLocaleMatcher.INSTANCE
private static final @NotNull PointersSupplier<VelocityConsole> POINTERS = PointersSupplier.<VelocityConsole>builder()
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
.lookupClosest(Locale.getDefault()))
.withStatic(FacetPointers.TYPE, Type.CONSOLE)
.resolving(FacetPointers.TYPE, (console) -> Type.CONSOLE)
.build();
public VelocityConsole(VelocityServer server) {
@@ -136,6 +137,10 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
if (!this.server.getCommandManager().executeAsync(this, command).join()) {
sendMessage(Component.translatable("velocity.command.command-does-not-exist",
NamedTextColor.RED));
return;
}
if (server.getConfiguration().isLogCommandExecutions()) {
logger.info("CONSOLE -> executed command /{}", command);
}
} catch (Exception e) {
logger.error("An error occurred while running this command.", e);
@@ -149,6 +154,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
@Override
public @NotNull Pointers pointers() {
return pointers;
return POINTERS.view(this);
}
}
@@ -26,8 +26,10 @@ import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
@@ -72,6 +74,17 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
new HandshakeSessionHandler(connection, this.server));
ch.pipeline().addLast(Connections.HANDLER, connection);
VelocityConfiguration.PacketLimiterConfig packetLimiterConfig =
server.getConfiguration().getPacketLimiterConfig();
int configuredInterval = packetLimiterConfig.interval();
int configuredPacketsPerSecond = packetLimiterConfig.pps();
int configuredBytes = packetLimiterConfig.bytes();
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
);
}
if (this.server.getConfiguration().isProxyProtocol()) {
ch.pipeline().addFirst(new HAProxyMessageDecoder());
}
@@ -116,7 +116,7 @@ public enum TransportType {
return NIO;
}
if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) {
if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) {
return IO_URING;
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2025 Velocity 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 com.velocitypowered.proxy.network.limiter;
/**
* PacketLimiter enforces a limit on the number of bytes processed over a time window.
* Implementations should be thread-safe.
*/
public interface PacketLimiter {
/**
* Attempts to record the specified number of bytes within the current window.
*
* @param bytes the number of bytes to record
* @return true if the bytes are allowed and recorded; false if the limit would be exceeded
*/
boolean account(int bytes);
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2025 Velocity 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 com.velocitypowered.proxy.network.limiter;
import com.velocitypowered.proxy.util.IntervalledCounter;
import org.jspecify.annotations.Nullable;
/**
* A moving-window limiter over a configurable number of seconds.
* It enforces both packets-per-second and average bytes-per-second limits.
* The effective cap over the full window equals limitPerSecond * windowSeconds.
*/
public final class SimpleBytesPerSecondLimiter implements PacketLimiter {
@Nullable
private final IntervalledCounter bytesCounter;
@Nullable
private final IntervalledCounter packetsCounter;
private final int packetsPerSecond;
private final int bytesPerSecond;
/**
* Creates a new SimpleBytesPerSecondLimiter.
*
* @param packetsPerSecond maximum average packets per second allowed (> 0)
* @param bytesPerSecond maximum average bytes per second allowed (> 0)
* @param windowSeconds number of seconds in the moving window (> 0)
*/
public SimpleBytesPerSecondLimiter(int packetsPerSecond, int bytesPerSecond, int windowSeconds) {
this.packetsPerSecond = packetsPerSecond;
if (windowSeconds <= 0) {
throw new IllegalArgumentException("windowSeconds must be > 0");
}
this.bytesPerSecond = bytesPerSecond;
this.packetsCounter = packetsPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
this.bytesCounter = bytesPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
}
/**
* Records the given payload length as one packet and returns whether it is allowed.
*/
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean account(int bytes) {
long currTime = System.nanoTime();
if (packetsCounter != null) {
packetsCounter.updateAndAdd(1, currTime);
if (packetsCounter.getRate() > packetsPerSecond) {
return false;
}
}
if (bytesCounter != null) {
bytesCounter.updateAndAdd(bytes, currTime);
if (bytesCounter.getRate() > bytesPerSecond) {
return false;
}
}
return true;
}
}
@@ -71,7 +71,7 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
List<InetAddress> addresses = cache.getIfPresent(inetHost);
if (addresses != null) {
promise.trySuccess(addresses.get(0));
promise.trySuccess(addresses.getFirst());
return;
}
@@ -83,7 +83,7 @@ public class JavaPluginLoader implements PluginLoader {
@Override
public PluginDescription createPluginFromCandidate(PluginDescription candidate) throws Exception {
if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate)) {
if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate candidateInst)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
@@ -93,8 +93,6 @@ public class JavaPluginLoader implements PluginLoader {
PluginClassLoader loader = new PluginClassLoader(new URL[]{pluginJarUrl});
loader.addToClassloaders();
JavaVelocityPluginDescriptionCandidate candidateInst =
(JavaVelocityPluginDescriptionCandidate) candidate;
Class<?> mainClass = loader.loadClass(candidateInst.getMainClass());
return createDescription(candidateInst, mainClass);
}
@@ -102,11 +100,10 @@ public class JavaPluginLoader implements PluginLoader {
@Override
public Module createModule(PluginContainer container) {
PluginDescription description = container.getDescription();
if (!(description instanceof JavaVelocityPluginDescription)) {
if (!(description instanceof JavaVelocityPluginDescription javaDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
Optional<Path> source = javaDescription.getSource();
if (source.isEmpty()) {
@@ -118,24 +115,23 @@ public class JavaPluginLoader implements PluginLoader {
@Override
public void createPlugin(PluginContainer container, Module... modules) {
if (!(container instanceof VelocityPluginContainer)) {
if (!(container instanceof VelocityPluginContainer pluginContainer)) {
throw new IllegalArgumentException("Container provided isn't of the Java plugin loader");
}
PluginDescription description = container.getDescription();
if (!(description instanceof JavaVelocityPluginDescription)) {
PluginDescription description = pluginContainer.getDescription();
if (!(description instanceof JavaVelocityPluginDescription javaPluginDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
}
Injector injector = Guice.createInjector(modules);
Object instance = injector
.getInstance(((JavaVelocityPluginDescription) description).getMainClass());
Object instance = injector.getInstance(javaPluginDescription.getMainClass());
if (instance == null) {
throw new IllegalStateException(
"Got nothing from injector for plugin " + description.getId());
}
((VelocityPluginContainer) container).setInstance(instance);
pluginContainer.setInstance(instance);
}
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source)
@@ -145,22 +141,23 @@ public class JavaPluginLoader implements PluginLoader {
new BufferedInputStream(Files.newInputStream(source)))) {
JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) {
if (entry.getName().equals("velocity-plugin.json")) {
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
SerializedPluginDescription.class));
switch (entry.getName()) {
case "velocity-plugin.json" -> {
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
SerializedPluginDescription.class));
}
}
case "paper-plugin.yml", "plugin.yml", "bungee.yml" -> foundBungeeBukkitPluginFile = true;
default -> {
}
}
if (entry.getName().equals("plugin.yml") || entry.getName().equals("bungee.yml")) {
foundBungeeBukkitPluginFile = true;
}
}
if (foundBungeeBukkitPluginFile) {
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
+ "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord "
+ "plugins.");
+ "be a Paper, Bukkit or BungeeCord plugin. Velocity does not support plugins from these "
+ "platforms.");
}
return Optional.empty();
@@ -32,13 +32,18 @@ public interface MinecraftPacket {
boolean handle(MinecraftSessionHandler handler);
default int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
default int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return -1;
}
default int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
default int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 0;
}
default int encodeSizeHint(ProtocolUtils.Direction direction,
ProtocolVersion version) {
return -1;
}
}
@@ -36,7 +36,9 @@ import io.netty.handler.codec.EncoderException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.BinaryTag;
@@ -44,6 +46,7 @@ import net.kyori.adventure.nbt.BinaryTagIO;
import net.kyori.adventure.nbt.BinaryTagType;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer;
@@ -61,6 +64,8 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// before 1.16
.value(JSONOptions.EMIT_RGB, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.VALUE_FIELD)
@@ -69,6 +74,8 @@ public enum ProtocolUtils {
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build()
)
.build();
@@ -77,6 +84,8 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
@@ -86,6 +95,8 @@ public enum ProtocolUtils {
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build()
)
.build();
@@ -94,6 +105,8 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
@@ -103,6 +116,8 @@ public enum ProtocolUtils {
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build()
)
.build();
@@ -111,6 +126,8 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.SNAKE_CASE)
@@ -121,6 +138,7 @@ public enum ProtocolUtils {
// after 1.21.5
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.FALSE)
.build()
)
.build();
@@ -134,7 +152,7 @@ public enum ProtocolUtils {
BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY};
private static final QuietDecoderException BAD_VARINT_CACHED =
new QuietDecoderException("Bad VarInt decoded");
private static final int[] VAR_INT_LENGTHS = new int[65];
private static final int[] VAR_INT_LENGTHS = new int[33];
static {
for (int i = 0; i <= 32; ++i) {
@@ -143,6 +161,9 @@ public enum ProtocolUtils {
VAR_INT_LENGTHS[32] = 1; // Special case for the number 0.
}
public static final int DEFAULT_MAX_STRING_BYTES = varIntBytes(ByteBufUtil.utf8MaxBytes(DEFAULT_MAX_STRING_SIZE))
+ ByteBufUtil.utf8MaxBytes(DEFAULT_MAX_STRING_SIZE);
private static DecoderException badVarint() {
return MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
: BAD_VARINT_CACHED;
@@ -234,16 +255,15 @@ public enum ProtocolUtils {
}
/**
* Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}.
* Directly encodes a 21-bit Minecraft VarInt, ready to be written with {@link ByteBuf#writeMedium(int)}.
* The upper 11 bits will be discarded.
*
* @param buf the buffer to read from
* @param value the integer to write
* @param value the value to encode
* @return the encoded value
*/
public static void write21BitVarInt(ByteBuf buf, int value) {
public static int encode21BitVarInt(int value) {
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buf.writeMedium(w);
return (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
}
public static String readString(ByteBuf buf) {
@@ -272,12 +292,22 @@ public enum ProtocolUtils {
checkFrame(buf.isReadable(length),
"Trying to read a string that is too long (wanted %s, only have %s)", length,
buf.readableBytes());
String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
String str = buf.readString(length, StandardCharsets.UTF_8);
checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap);
return str;
}
/**
* Determines the size of the written {@code str} if encoded as a VarInt-prefixed UTF-8 string.
*
* @param str the string to write
* @return the encoded size
*/
public static int stringSizeHint(CharSequence str) {
int size = ByteBufUtil.utf8Bytes(str);
return varIntBytes(size) + size;
}
/**
* Writes the specified {@code str} to the {@code buf} with a VarInt prefix.
*
@@ -310,6 +340,16 @@ public enum ProtocolUtils {
writeString(buf, key.asString());
}
/**
* Writes the key to the buffer, dropping the "minecraft:" namespace when present.
*
* @param buf the buffer to write to
* @param key the key to write
*/
public static void writeMinimalKey(ByteBuf buf, Key key) {
writeString(buf, key.asMinimalString());
}
/**
* Reads a standard Mojang Text namespaced:key array from the buffer.
*
@@ -380,7 +420,10 @@ public enum ProtocolUtils {
*/
public static int[] readIntegerArray(ByteBuf buf) {
int len = readVarInt(buf);
checkArgument(len >= 0, "Got a negative-length integer array (%s)", len);
checkFrame(len >= 0, "Got a negative-length integer array (%s)", len);
checkFrame(buf.isReadable(len),
"Trying to read an array that is too long (wanted %s, only have %s)", len,
buf.readableBytes());
int[] array = new int[len];
for (int i = 0; i < len; i++) {
array[i] = readVarInt(buf);
@@ -500,6 +543,10 @@ public enum ProtocolUtils {
*/
public static String[] readStringArray(ByteBuf buf) {
int length = readVarInt(buf);
checkFrame(length >= 0, "Got a negative-length array (%s)", length);
checkFrame(buf.isReadable(length),
"Trying to read an array that is too long (wanted %s, only have %s)", length,
buf.readableBytes());
String[] ret = new String[length];
for (int i = 0; i < length; i++) {
ret[i] = readString(buf);
@@ -611,6 +658,9 @@ public enum ProtocolUtils {
checkArgument(len <= FORGE_MAX_ARRAY_LENGTH,
"Cannot receive array longer than %s (got %s bytes)", FORGE_MAX_ARRAY_LENGTH, len);
checkFrame(buf.isReadable(len),
"Trying to read an array that is too long (wanted %s, only have %s)", len,
buf.readableBytes());
byte[] ret = new byte[len];
buf.readBytes(ret);
@@ -775,6 +825,63 @@ public enum ProtocolUtils {
return new IdentifiedKeyImpl(revision, key, expiry, signature);
}
/**
* Reads a {@link Sound.Source} from the buffer.
*
* @param buf the buffer
* @param version the protocol version
* @return the sound source
*/
public static Sound.Source readSoundSource(ByteBuf buf, ProtocolVersion version) {
int ordinal = readVarInt(buf);
if (version.lessThan(ProtocolVersion.MINECRAFT_1_21_5)
&& ordinal == Sound.Source.UI.ordinal()) {
throw new UnsupportedOperationException("UI sound-source is only supported in 1.21.5+");
}
return Sound.Source.values()[ordinal];
}
/**
* Writes a {@link Sound.Source} to the buffer.
*
* @param buf the buffer
* @param version the protocol version
* @param source the sound source to write
*/
public static void writeSoundSource(ByteBuf buf, ProtocolVersion version, Sound.Source source) {
if (version.lessThan(ProtocolVersion.MINECRAFT_1_21_5)
&& source == Sound.Source.UI) {
throw new UnsupportedOperationException("UI sound-source is only supported in 1.21.5+");
}
writeVarInt(buf, source.ordinal());
}
/**
* Returns a pre-sized list with a max initial size of {@code Short.MAX_VALUE}.
*
* @param initialCapacity expected initial capacity
* @param <T> entry type
* @return pre-sized list
*/
public static <T> List<T> newList(int initialCapacity) {
return new ArrayList<>(Math.min(initialCapacity, Short.MAX_VALUE));
}
/**
* Returns a pre-sized map with a max initial size of {@code Short.MAX_VALUE}.
*
* @param initialCapacity expected initial capacity
* @param <K> key type
* @param <V> value type
* @return pre-sized map
*/
public static <K, V> Map<K, V> newMap(int initialCapacity) {
return new HashMap<>(Math.min(initialCapacity, Short.MAX_VALUE));
}
/**
* Represents the direction in which a packet flows.
*/
@@ -782,4 +889,4 @@ public enum ProtocolUtils {
SERVERBOUND,
CLIENTBOUND
}
}
}
@@ -41,10 +41,12 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION;
import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
@@ -58,7 +60,11 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DialogClearPacket;
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
@@ -81,6 +87,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -88,6 +95,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.UpdateTeamsPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
@@ -101,6 +109,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.UnsignedPlayerComm
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
@@ -183,6 +193,12 @@ public enum StateRegistry {
KnownPacksPacket.class,
KnownPacksPacket::new,
map(0x07, MINECRAFT_1_20_5, false));
serverbound.register(ServerboundCustomClickActionPacket.class, ServerboundCustomClickActionPacket::new,
map(0x08, MINECRAFT_1_21_6, false));
serverbound.register(
CodeOfConductAcceptPacket.class,
() -> CodeOfConductAcceptPacket.INSTANCE,
map(0x09, MINECRAFT_1_21_9, false));
clientbound.register(
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
@@ -237,6 +253,12 @@ public enum StateRegistry {
map(0x0F, MINECRAFT_1_21, false));
clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new,
map(0x10, MINECRAFT_1_21, false));
clientbound.register(DialogClearPacket.class, () -> DialogClearPacket.INSTANCE,
map(0x11, MINECRAFT_1_21_6, false));
clientbound.register(DialogShowPacket.class, () -> new DialogShowPacket(this),
map(0x12, MINECRAFT_1_21_6, false));
clientbound.register(CodeOfConductPacket.class, CodeOfConductPacket::new,
map(0x13, MINECRAFT_1_21_9, false));
}
},
PLAY {
@@ -258,7 +280,8 @@ public enum StateRegistry {
map(0x0A, MINECRAFT_1_20_2, false),
map(0x0B, MINECRAFT_1_20_5, false),
map(0x0D, MINECRAFT_1_21_2, false),
map(0x0E, MINECRAFT_1_21_6, false));
map(0x0E, MINECRAFT_1_21_6, false),
map(0x0F, MINECRAFT_26_1, false));
serverbound.register(
LegacyChatPacket.class,
LegacyChatPacket::new,
@@ -272,7 +295,8 @@ public enum StateRegistry {
ChatAcknowledgementPacket::new,
map(0x03, MINECRAFT_1_19_3, false),
map(0x04, MINECRAFT_1_21_2, false),
map(0x05, MINECRAFT_1_21_6, false));
map(0x05, MINECRAFT_1_21_6, false),
map(0x06, MINECRAFT_26_1, false));
serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new,
map(0x03, MINECRAFT_1_19, false),
map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false));
@@ -283,18 +307,21 @@ public enum StateRegistry {
map(0x04, MINECRAFT_1_19_3, false),
map(0x05, MINECRAFT_1_20_5, false),
map(0x06, MINECRAFT_1_21_2, false),
map(0x07, MINECRAFT_1_21_6, false));
map(0x07, MINECRAFT_1_21_6, false),
map(0x08, MINECRAFT_26_1, false));
serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new,
map(0x04, MINECRAFT_1_20_5, false),
map(0x05, MINECRAFT_1_21_2, false),
map(0x06, MINECRAFT_1_21_6, false));
map(0x06, MINECRAFT_1_21_6, false),
map(0x07, MINECRAFT_26_1, false));
serverbound.register(
SessionPlayerChatPacket.class,
SessionPlayerChatPacket::new,
map(0x05, MINECRAFT_1_19_3, false),
map(0x06, MINECRAFT_1_20_5, false),
map(0x07, MINECRAFT_1_21_2, false),
map(0x08, MINECRAFT_1_21_6, false));
map(0x08, MINECRAFT_1_21_6, false),
map(0x09, MINECRAFT_26_1, false));
serverbound.register(
ClientSettingsPacket.class,
ClientSettingsPacket::new,
@@ -310,12 +337,14 @@ public enum StateRegistry {
map(0x09, MINECRAFT_1_20_2, false),
map(0x0A, MINECRAFT_1_20_5, false),
map(0x0C, MINECRAFT_1_21_2, false),
map(0x0D, MINECRAFT_1_21_6, false));
map(0x0D, MINECRAFT_1_21_6, false),
map(0x0E, MINECRAFT_26_1, false));
serverbound.register(
ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new,
map(0x11, MINECRAFT_1_20_5, false),
map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false));
map(0x14, MINECRAFT_1_21_6, false),
map(0x15, MINECRAFT_26_1, false));
serverbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
@@ -334,7 +363,8 @@ public enum StateRegistry {
map(0x10, MINECRAFT_1_20_3, false),
map(0x12, MINECRAFT_1_20_5, false),
map(0x14, MINECRAFT_1_21_2, false),
map(0x15, MINECRAFT_1_21_6, false));
map(0x15, MINECRAFT_1_21_6, false),
map(0x16, MINECRAFT_26_1, false));
serverbound.register(
KeepAlivePacket.class,
KeepAlivePacket::new,
@@ -354,7 +384,8 @@ public enum StateRegistry {
map(0x15, MINECRAFT_1_20_3, false),
map(0x18, MINECRAFT_1_20_5, false),
map(0x1A, MINECRAFT_1_21_2, false),
map(0x1B, MINECRAFT_1_21_6, false));
map(0x1B, MINECRAFT_1_21_6, false),
map(0x1C, MINECRAFT_26_1, false));
serverbound.register(
ResourcePackResponsePacket.class,
ResourcePackResponsePacket::new,
@@ -372,13 +403,15 @@ public enum StateRegistry {
map(0x2B, MINECRAFT_1_20_5, false),
map(0x2D, MINECRAFT_1_21_2, false),
map(0x2F, MINECRAFT_1_21_4, false),
map(0x30, MINECRAFT_1_21_6, false));
map(0x30, MINECRAFT_1_21_6, false),
map(0x31, MINECRAFT_26_1, false));
serverbound.register(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, false),
map(0x0C, MINECRAFT_1_20_5, false),
map(0x0E, MINECRAFT_1_21_2, false),
map(0x0F, MINECRAFT_1_21_6, false));
map(0x0F, MINECRAFT_1_21_6, false),
map(0x10, MINECRAFT_26_1, false));
clientbound.register(
BossBarPacket.class,
@@ -430,6 +463,28 @@ public enum StateRegistry {
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
map(0x16, MINECRAFT_1_20_5, false),
map(0x15, MINECRAFT_1_21_5, false));
clientbound.register(
ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new,
map(0x5D, MINECRAFT_1_19_3, true),
map(0x61, MINECRAFT_1_19_4, true),
map(0x63, MINECRAFT_1_20_2, true),
map(0x65, MINECRAFT_1_20_3, true),
map(0x67, MINECRAFT_1_20_5, true),
map(0x6E, MINECRAFT_1_21_2, true),
map(0x6D, MINECRAFT_1_21_5, true),
map(0x72, MINECRAFT_1_21_9, true),
map(0x74, MINECRAFT_26_1, true));
clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true),
map(0x63, MINECRAFT_1_19_4, true),
map(0x66, MINECRAFT_1_20_2, true),
map(0x68, MINECRAFT_1_20_3, true),
map(0x6A, MINECRAFT_1_20_5, true),
map(0x71, MINECRAFT_1_21_2, true),
map(0x70, MINECRAFT_1_21_5, true),
map(0x75, MINECRAFT_1_21_9, true),
map(0x77, MINECRAFT_26_1, true));
clientbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
@@ -465,7 +520,8 @@ public enum StateRegistry {
map(0x1A, MINECRAFT_1_19_4, false),
map(0x1B, MINECRAFT_1_20_2, false),
map(0x1D, MINECRAFT_1_20_5, false),
map(0x1C, MINECRAFT_1_21_5, false));
map(0x1C, MINECRAFT_1_21_5, false),
map(0x20, MINECRAFT_1_21_9, false));
clientbound.register(
KeepAlivePacket.class,
KeepAlivePacket::new,
@@ -484,7 +540,9 @@ public enum StateRegistry {
map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, false),
map(0x27, MINECRAFT_1_21_2, false),
map(0x26, MINECRAFT_1_21_5, false));
map(0x26, MINECRAFT_1_21_5, false),
map(0x2B, MINECRAFT_1_21_9, false),
map(0x2C, MINECRAFT_26_1, false));
clientbound.register(
JoinGamePacket.class,
JoinGamePacket::new,
@@ -503,7 +561,9 @@ public enum StateRegistry {
map(0x29, MINECRAFT_1_20_2, false),
map(0x2B, MINECRAFT_1_20_5, false),
map(0x2C, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_5, false));
map(0x2B, MINECRAFT_1_21_5, false),
map(0x30, MINECRAFT_1_21_9, false),
map(0x31, MINECRAFT_26_1, false));
clientbound.register(
RespawnPacket.class,
RespawnPacket::new,
@@ -525,14 +585,18 @@ public enum StateRegistry {
map(0x45, MINECRAFT_1_20_3, true),
map(0x47, MINECRAFT_1_20_5, true),
map(0x4C, MINECRAFT_1_21_2, true),
map(0x4B, MINECRAFT_1_21_5, true));
map(0x4B, MINECRAFT_1_21_5, true),
map(0x50, MINECRAFT_1_21_9, true),
map(0x52, MINECRAFT_26_1, true));
clientbound.register(
RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new,
map(0x43, MINECRAFT_1_20_3, false),
map(0x45, MINECRAFT_1_20_5, false),
map(0x4A, MINECRAFT_1_21_2, false),
map(0x49, MINECRAFT_1_21_5, false));
map(0x49, MINECRAFT_1_21_5, false),
map(0x4E, MINECRAFT_1_21_9, false),
map(0x50, MINECRAFT_26_1, false));
clientbound.register(
ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new,
@@ -554,7 +618,9 @@ public enum StateRegistry {
map(0x44, MINECRAFT_1_20_3, false),
map(0x46, MINECRAFT_1_20_5, false),
map(0x4B, MINECRAFT_1_21_2, false),
map(0x4A, MINECRAFT_1_21_5, false));
map(0x4A, MINECRAFT_1_21_5, false),
map(0x4F, MINECRAFT_1_21_9, false),
map(0x51, MINECRAFT_26_1, false));
clientbound.register(
HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new,
@@ -577,7 +643,9 @@ public enum StateRegistry {
map(0x6A, MINECRAFT_1_20_3, true),
map(0x6D, MINECRAFT_1_20_5, true),
map(0x74, MINECRAFT_1_21_2, true),
map(0x73, MINECRAFT_1_21_5, true));
map(0x73, MINECRAFT_1_21_5, true),
map(0x78, MINECRAFT_1_21_9, true),
map(0x7A, MINECRAFT_26_1, true));
clientbound.register(
LegacyTitlePacket.class,
LegacyTitlePacket::new,
@@ -599,7 +667,9 @@ public enum StateRegistry {
map(0x61, MINECRAFT_1_20_3, true),
map(0x63, MINECRAFT_1_20_5, true),
map(0x6A, MINECRAFT_1_21_2, true),
map(0x69, MINECRAFT_1_21_5, true));
map(0x69, MINECRAFT_1_21_5, true),
map(0x6E, MINECRAFT_1_21_9, true),
map(0x70, MINECRAFT_26_1, true));
clientbound.register(
TitleTextPacket.class,
TitleTextPacket::new,
@@ -612,7 +682,9 @@ public enum StateRegistry {
map(0x63, MINECRAFT_1_20_3, true),
map(0x65, MINECRAFT_1_20_5, true),
map(0x6C, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_5, true));
map(0x6B, MINECRAFT_1_21_5, true),
map(0x70, MINECRAFT_1_21_9, true),
map(0x72, MINECRAFT_26_1, true));
clientbound.register(
TitleActionbarPacket.class,
TitleActionbarPacket::new,
@@ -625,7 +697,9 @@ public enum StateRegistry {
map(0x4A, MINECRAFT_1_20_3, true),
map(0x4C, MINECRAFT_1_20_5, true),
map(0x51, MINECRAFT_1_21_2, true),
map(0x50, MINECRAFT_1_21_5, true));
map(0x50, MINECRAFT_1_21_5, true),
map(0x55, MINECRAFT_1_21_9, true),
map(0x57, MINECRAFT_26_1, true));
clientbound.register(
TitleTimesPacket.class,
TitleTimesPacket::new,
@@ -638,7 +712,9 @@ public enum StateRegistry {
map(0x64, MINECRAFT_1_20_3, true),
map(0x66, MINECRAFT_1_20_5, true),
map(0x6D, MINECRAFT_1_21_2, true),
map(0x6C, MINECRAFT_1_21_5, true));
map(0x6C, MINECRAFT_1_21_5, true),
map(0x71, MINECRAFT_1_21_9, true),
map(0x73, MINECRAFT_26_1, true));
clientbound.register(
TitleClearPacket.class,
TitleClearPacket::new,
@@ -668,7 +744,9 @@ public enum StateRegistry {
map(0x3B, MINECRAFT_1_20_2, false),
map(0x3D, MINECRAFT_1_20_5, false),
map(0x3F, MINECRAFT_1_21_2, false),
map(0x3E, MINECRAFT_1_21_5, false));
map(0x3E, MINECRAFT_1_21_5, false),
map(0x43, MINECRAFT_1_21_9, false),
map(0x45, MINECRAFT_26_1, false));
clientbound.register(
UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new,
@@ -677,12 +755,16 @@ public enum StateRegistry {
map(0x3C, MINECRAFT_1_20_2, false),
map(0x3E, MINECRAFT_1_20_5, false),
map(0x40, MINECRAFT_1_21_2, false),
map(0x3F, MINECRAFT_1_21_5, false));
map(0x3F, MINECRAFT_1_21_5, false),
map(0x44, MINECRAFT_1_21_9, false),
map(0x46, MINECRAFT_26_1, false));
clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false),
map(0x72, MINECRAFT_1_21_2, false),
map(0x71, MINECRAFT_1_21_5, false));
map(0x71, MINECRAFT_1_21_5, false),
map(0x76, MINECRAFT_1_21_9, false),
map(0x78, MINECRAFT_26_1, false));
clientbound.register(
SystemChatPacket.class,
SystemChatPacket::new,
@@ -694,7 +776,9 @@ public enum StateRegistry {
map(0x69, MINECRAFT_1_20_3, true),
map(0x6C, MINECRAFT_1_20_5, true),
map(0x73, MINECRAFT_1_21_2, true),
map(0x72, MINECRAFT_1_21_5, true));
map(0x72, MINECRAFT_1_21_5, true),
map(0x77, MINECRAFT_1_21_9, true),
map(0x79, MINECRAFT_26_1, true));
clientbound.register(
PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new,
@@ -715,7 +799,9 @@ public enum StateRegistry {
map(0x49, MINECRAFT_1_20_3, false),
map(0x4B, MINECRAFT_1_20_5, false),
map(0x50, MINECRAFT_1_21_2, false),
map(0x4F, MINECRAFT_1_21_5, false));
map(0x4F, MINECRAFT_1_21_5, false),
map(0x54, MINECRAFT_1_21_9, false),
map(0x56, MINECRAFT_26_1, false));
clientbound.register(
StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE,
@@ -723,7 +809,9 @@ public enum StateRegistry {
map(0x67, MINECRAFT_1_20_3, false),
map(0x69, MINECRAFT_1_20_5, false),
map(0x70, MINECRAFT_1_21_2, false),
map(0x6F, MINECRAFT_1_21_5, false));
map(0x6F, MINECRAFT_1_21_5, false),
map(0x74, MINECRAFT_1_21_9, false),
map(0x76, MINECRAFT_26_1, false));
clientbound.register(
BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE,
@@ -732,17 +820,39 @@ public enum StateRegistry {
TransferPacket.class,
TransferPacket::new,
map(0x73, MINECRAFT_1_20_5, false),
map(0x7A, MINECRAFT_1_21_2, false));
map(0x7A, MINECRAFT_1_21_2, false),
map(0x7F, MINECRAFT_1_21_9, false),
map(0x81, MINECRAFT_26_1, false));
clientbound.register(
ClientboundCustomReportDetailsPacket.class,
ClientboundCustomReportDetailsPacket::new,
map(0x7A, MINECRAFT_1_21, false),
map(0x81, MINECRAFT_1_21_2, false));
map(0x81, MINECRAFT_1_21_2, false),
map(0x86, MINECRAFT_1_21_9, false),
map(0x88, MINECRAFT_26_1, false));
clientbound.register(
ClientboundServerLinksPacket.class,
ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false));
map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false));
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
map(0x41, ProtocolVersion.MINECRAFT_1_9, true),
map(0x43, ProtocolVersion.MINECRAFT_1_12, true),
map(0x44, ProtocolVersion.MINECRAFT_1_12_1, true),
map(0x47, ProtocolVersion.MINECRAFT_1_13, true),
map(0x4B, ProtocolVersion.MINECRAFT_1_14, true),
map(0x4C, ProtocolVersion.MINECRAFT_1_15, true),
map(0x55, ProtocolVersion.MINECRAFT_1_17, true),
map(0x58, ProtocolVersion.MINECRAFT_1_19_1, true),
map(0x56, ProtocolVersion.MINECRAFT_1_19_3, true),
map(0x5A, ProtocolVersion.MINECRAFT_1_19_4, true),
map(0x5C, ProtocolVersion.MINECRAFT_1_20_2, true),
map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true),
map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true),
map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true)
map(0x87, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false));
}
},
LOGIN {
@@ -771,7 +881,7 @@ public enum StateRegistry {
map(0x01, MINECRAFT_1_7_2, false));
clientbound.register(
ServerLoginSuccessPacket.class, ServerLoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, false));
map(0x02, MINECRAFT_1_7_2, false));
clientbound.register(
SetCompressionPacket.class, SetCompressionPacket::new,
map(0x03, MINECRAFT_1_8, false));
@@ -119,7 +119,7 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
int sessionId = queryMessage.readInt();
switch (type) {
case QUERY_TYPE_HANDSHAKE: {
case QUERY_TYPE_HANDSHAKE -> {
// Generate new challenge token and put it into the sessions cache
int challengeToken = random.nextInt();
sessions.put(senderAddress, challengeToken);
@@ -132,10 +132,9 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender());
ctx.writeAndFlush(responsePacket, ctx.voidPromise());
break;
}
case QUERY_TYPE_STAT: {
case QUERY_TYPE_STAT -> {
// Check if query was done with session previously generated using a handshake packet
int challengeToken = queryMessage.readInt();
Integer session = sessions.getIfPresent(senderAddress);
@@ -190,10 +189,10 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
"Exception while writing GS4 response for query from {}", senderAddress, ex);
return null;
});
break;
}
default:
// Invalid query type - just don't respond
default -> {
// Invalid query type - just don't respond
}
}
}
@@ -33,20 +33,34 @@ import java.util.List;
*/
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB
private static final int UNCOMPRESSED_CAP =
private static final int CLIENTBOUND_UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
private static final int SERVERBOUND_UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
private static final double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
private final ProtocolUtils.Direction direction;
private int threshold;
private final VelocityCompressor compressor;
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
/**
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
*
* @param threshold the threshold for compression. Packets with uncompressed size below this threshold will not be compressed.
* @param compressor the compressor instance to use
* @param direction the direction of the packets being decoded
*/
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor, ProtocolUtils.Direction direction) {
this.threshold = threshold;
this.compressor = compressor;
this.direction = direction;
}
@Override
@@ -62,17 +76,29 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
out.add(in.retain());
return;
}
int length = in.readableBytes();
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
+ " threshold %s", claimedUncompressedSize, threshold);
checkFrame(claimedUncompressedSize <= UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
UNCOMPRESSED_CAP);
if (direction == ProtocolUtils.Direction.CLIENTBOUND) {
checkFrame(claimedUncompressedSize <= CLIENTBOUND_UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
CLIENTBOUND_UNCOMPRESSED_CAP);
} else {
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
SERVERBOUND_UNCOMPRESSED_CAP);
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
maxCompressedAllowed, length);
}
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
try {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
out.add(uncompressed);
} catch (Exception e) {
uncompressed.release();
@@ -46,7 +46,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
if (uncompressed < threshold) {
// Under the threshold, there is nothing to do.
ProtocolUtils.writeVarInt(out, uncompressed + 1);
ProtocolUtils.writeVarInt(out, 0);
out.writeByte(0);
out.writeBytes(msg);
} else {
handleCompressed(ctx, msg, out);
@@ -57,7 +57,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
throws DataFormatException {
int uncompressed = msg.readableBytes();
ProtocolUtils.write21BitVarInt(out, 0); // Dummy packet length
out.writeMedium(0); // Reserve the packet length
ProtocolUtils.writeVarInt(out, uncompressed);
ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
@@ -72,11 +72,8 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
}
int writerIndex = out.writerIndex();
int packetLength = out.readableBytes() - 3;
out.writerIndex(0);
ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length
out.writerIndex(writerIndex);
out.setMedium(0, ProtocolUtils.encode21BitVarInt(packetLength)); // Rewrite packet length
}
@Override
@@ -57,7 +57,11 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf buf) {
tryDecode(ctx, buf);
try {
tryDecode(ctx, buf);
} finally {
buf.release();
}
} else {
ctx.fireChannelRead(msg);
}
@@ -65,7 +69,6 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
if (!ctx.channel().isActive() || !buf.isReadable()) {
buf.release();
return;
}
@@ -74,30 +77,29 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
MinecraftPacket packet = this.registry.createPacket(packetId);
if (packet == null) {
buf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(buf);
} else {
try {
doLengthSanityChecks(buf, packet);
try {
packet.decode(buf, direction, registry.version);
} catch (Exception e) {
throw handleDecodeFailure(e, packet, packetId);
}
if (buf.isReadable()) {
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
}
ctx.fireChannelRead(packet);
} finally {
buf.release();
if (this.direction == ProtocolUtils.Direction.SERVERBOUND && this.state != StateRegistry.PLAY) {
throw this.handleInvalidPacketId(packetId);
}
ctx.fireChannelRead(buf.retain());
} else {
doLengthSanityChecks(buf, packet);
try {
packet.decode(buf, direction, registry.version);
} catch (Exception e) {
throw handleDecodeFailure(e, packet, packetId);
}
if (buf.isReadable()) {
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
}
ctx.fireChannelRead(packet);
}
}
private void doLengthSanityChecks(ByteBuf buf, MinecraftPacket packet) throws Exception {
int expectedMinLen = packet.expectedMinLength(buf, direction, registry.version);
int expectedMaxLen = packet.expectedMaxLength(buf, direction, registry.version);
int expectedMinLen = packet.decodeExpectedMinLength(buf, direction, registry.version);
int expectedMaxLen = packet.decodeExpectedMaxLength(buf, direction, registry.version);
if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
}
@@ -133,6 +135,14 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
}
}
private Exception handleInvalidPacketId(int packetId) {
if (DEBUG) {
return new CorruptedFrameException("Invalid packet " + getExtraConnectionDetail(packetId));
} else {
return DECODE_FAILED;
}
}
private String getExtraConnectionDetail(int packetId) {
return "Direction " + direction + " Protocol " + registry.version + " State " + state
+ " ID 0x" + Integer.toHexString(packetId);
@@ -54,6 +54,19 @@ public class MinecraftEncoder extends MessageToByteEncoder<MinecraftPacket> {
msg.encode(out, direction, registry.version);
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, MinecraftPacket msg,
boolean preferDirect) throws Exception {
int hint = msg.encodeSizeHint(direction, registry.version);
if (hint < 0) {
return super.allocateBuffer(ctx, msg, preferDirect);
}
int packetId = this.registry.getPacketId(msg);
int totalHint = ProtocolUtils.varIntBytes(packetId) + hint;
return preferDirect ? ctx.alloc().ioBuffer(totalHint) : ctx.alloc().heapBuffer(totalHint);
}
public void setProtocolVersion(final ProtocolVersion protocolVersion) {
this.registry = state.getProtocolRegistry(direction, protocolVersion);
}
@@ -20,6 +20,7 @@ package com.velocitypowered.proxy.protocol.netty;
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
@@ -32,6 +33,7 @@ import io.netty.handler.codec.CorruptedFrameException;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.Nullable;
/**
* Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding.
@@ -44,6 +46,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
+ "Velocity with -Dvelocity.packet-decode-logging=true to see more.");
private static final QuietDecoderException BAD_PACKET_LENGTH =
new QuietDecoderException("Bad packet length");
private static final QuietDecoderException INVALID_PREAMBLE =
new QuietDecoderException("Invalid packet preamble");
private static final QuietDecoderException VARINT_TOO_BIG =
new QuietDecoderException("VarInt too big");
private static final QuietDecoderException UNKNOWN_PACKET =
@@ -52,6 +56,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
private final ProtocolUtils.Direction direction;
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
private StateRegistry state;
@Nullable
private PacketLimiter packetLimiter;
/**
* Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}.
@@ -74,69 +80,93 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
}
// skip any runs of 0x00 we might find
int wlen = in.readableBytes();
int packetStart = in.forEachByte(FIND_NON_NUL);
if (packetStart == -1) {
in.clear();
// Apply a more strict check in serverbound direction, we really shouldn't be seeing this many 0x00s
// even from the server, the only reason we even allow these is due to bugged servers
if (direction == ProtocolUtils.Direction.SERVERBOUND && wlen > 16) {
throw INVALID_PREAMBLE;
}
return;
}
in.readerIndex(packetStart);
// try to read the length of the packet
in.markReaderIndex();
int preIndex = in.readerIndex();
int length = readRawVarInt21(in);
if (preIndex == in.readerIndex()) {
return;
}
if (length < 0) {
throw BAD_PACKET_LENGTH;
}
if (length > 0) {
if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) {
StateRegistry.PacketRegistry.ProtocolRegistry registry =
state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION);
final int index = in.readerIndex();
final int packetId = readRawVarInt21(in);
// Index hasn't changed, we've read nothing
if (index == in.readerIndex()) {
in.resetReaderIndex();
return;
}
final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
MinecraftPacket packet = registry.createPacket(packetId);
// We handle every packet in this phase, if you said something we don't know, something is really wrong
if (packet == null) {
throw UNKNOWN_PACKET;
}
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse
// the packet if needed, so, we'll take advantage of the existing methods
int expectedMinLen = packet.expectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.expectedMaxLength(in, direction, registry.version);
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
}
if (payloadLength < expectedMinLen) {
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes());
}
in.readerIndex(index);
try {
int length = readRawVarInt21(in);
if (packetStart == in.readerIndex()) {
return;
}
if (length < 0) {
throw BAD_PACKET_LENGTH;
}
if (length > 0) {
if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) {
if (validateServerboundHandshakePacket(in, length)) {
in.readerIndex(packetStart);
return;
}
}
}
// note that zero-length packets are ignored
if (length > 0) {
if (in.readableBytes() < length) {
in.readerIndex(packetStart);
} else {
// If enabled, rate-limit serverbound payload bytes based on frame length
if (packetLimiter != null) {
if (!packetLimiter.account(length)) {
throw new QuietDecoderException(
"Rate limit exceeded while processing packets for %s".formatted(
ctx.channel().remoteAddress()));
}
}
out.add(in.readRetainedSlice(length));
}
}
} catch (Exception e) {
// Reset buffer to consistent state before propagating exception to prevent memory leaks
in.readerIndex(packetStart);
throw e;
}
}
private boolean validateServerboundHandshakePacket(ByteBuf in, int length) throws Exception {
StateRegistry.PacketRegistry.ProtocolRegistry registry =
state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION);
final int index = in.readerIndex();
final int packetId = readRawVarInt21(in);
// Index hasn't changed, we've read nothing
if (index == in.readerIndex()) {
return true;
}
final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
MinecraftPacket packet = registry.createPacket(packetId);
// We handle every packet in this phase, if you said something we don't know, something is really wrong
if (packet == null) {
throw UNKNOWN_PACKET;
}
// note that zero-length packets are ignored
if (length > 0) {
if (in.readableBytes() < length) {
in.resetReaderIndex();
} else {
out.add(in.readRetainedSlice(length));
}
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse
// the packet if needed, so, we'll take advantage of the existing methods
int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version);
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
}
if (payloadLength < expectedMinLen) {
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes());
}
in.readerIndex(index);
return false;
}
@Override
@@ -240,4 +270,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
public void setState(StateRegistry stateRegistry) {
this.state = stateRegistry;
}
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
this.packetLimiter = packetLimiter;
}
}
@@ -21,6 +21,9 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;
@@ -41,8 +44,13 @@ import org.jetbrains.annotations.NotNull;
*/
public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
private static final int MAXIMUM_SIZE = Integer.getInteger("velocity.maximum-play-queue-size", 128 * 1024 * 1024); // 128MiB by default
private static final QuietDecoderException QUEUE_LIMIT_FAILED = new QuietDecoderException(
"Queue too big (greater than " + MAXIMUM_SIZE + " bytes)");
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
private final Queue<Object> queue = new ArrayDeque<>();
private int queueSize = 0;
/**
* Provides registries for client &amp; server bound packets.
@@ -64,6 +72,20 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
}
}
int length = 0;
if (msg instanceof ByteBuf) {
// keep track of raw packets
length = ((ByteBuf) msg).readableBytes();
} else if (msg instanceof ByteBufHolder) {
// keep track of bytebufs wrapped inside packets
length = ((ByteBufHolder) msg).content().readableBytes();
}
if (this.queueSize + length > MAXIMUM_SIZE) {
ReferenceCountUtil.release(msg);
throw QUEUE_LIMIT_FAILED;
}
this.queueSize += length;
// Otherwise, queue the packet
this.queue.offer(msg);
}
@@ -90,5 +112,6 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
ReferenceCountUtil.release(msg);
}
}
this.queueSize = 0;
}
}
@@ -25,7 +25,6 @@ import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
@@ -45,9 +44,9 @@ import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
@@ -86,14 +85,14 @@ public class AvailableCommandsPacket implements MinecraftPacket {
@Override
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
int commands = ProtocolUtils.readVarInt(buf);
WireNode[] wireNodes = new WireNode[commands];
List<WireNode> wireNodes = ProtocolUtils.newList(commands);
for (int i = 0; i < commands; i++) {
wireNodes[i] = deserializeNode(buf, i, protocolVersion);
wireNodes.add(deserializeNode(buf, i, protocolVersion));
}
// Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles
// that exist.
Queue<WireNode> nodeQueue = new ArrayDeque<>(Arrays.asList(wireNodes));
Queue<WireNode> nodeQueue = new ArrayDeque<>(wireNodes);
while (!nodeQueue.isEmpty()) {
boolean cycling = false;
@@ -112,7 +111,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
}
int rootIdx = ProtocolUtils.readVarInt(buf);
rootNode = (RootCommandNode<CommandSource>) wireNodes[rootIdx].built;
rootNode = (RootCommandNode<CommandSource>) wireNodes.get(rootIdx).built;
}
@Override
@@ -246,17 +245,17 @@ public class AvailableCommandsPacket implements MinecraftPacket {
this.validated = false;
}
void validate(WireNode[] wireNodes) {
void validate(List<WireNode> wireNodes) {
// Ensure all children exist. Note that we delay checking if the node has been built yet;
// that needs to come after this node is built.
for (int child : children) {
if (child < 0 || child >= wireNodes.length) {
if (child < 0 || child >= wireNodes.size()) {
throw new IllegalStateException("Node points to non-existent index " + child);
}
}
if (redirectTo != -1) {
if (redirectTo < 0 || redirectTo >= wireNodes.length) {
if (redirectTo < 0 || redirectTo >= wireNodes.size()) {
throw new IllegalStateException("Redirect node points to non-existent index "
+ redirectTo);
}
@@ -265,7 +264,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
this.validated = true;
}
boolean toNode(WireNode[] wireNodes) {
boolean toNode(List<WireNode> wireNodes) {
if (!this.validated) {
this.validate(wireNodes);
}
@@ -281,7 +280,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
// Add any redirects
if (redirectTo != -1) {
WireNode redirect = wireNodes[redirectTo];
WireNode redirect = wireNodes.get(redirectTo);
if (redirect.built != null) {
args.redirect(redirect.built);
} else {
@@ -305,7 +304,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
}
for (int child : children) {
if (wireNodes[child].built == null) {
if (wireNodes.get(child).built == null) {
// The child is not yet deserialized. The node can't be built now.
return false;
}
@@ -313,7 +312,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
// Associate children with nodes
for (int child : children) {
CommandNode<CommandSource> childNode = wireNodes[child].built;
CommandNode<CommandSource> childNode = wireNodes.get(child).built;
if (!(childNode instanceof RootCommandNode)) {
built.addChild(childNode);
}
@@ -331,12 +330,10 @@ public class AvailableCommandsPacket implements MinecraftPacket {
.add("redirectTo", redirectTo);
if (args != null) {
if (args instanceof LiteralArgumentBuilder) {
helper.add("argsLabel",
((LiteralArgumentBuilder<CommandSource>) args).getLiteral());
} else if (args instanceof RequiredArgumentBuilder) {
helper.add("argsName",
((RequiredArgumentBuilder<CommandSource, ?>) args).getName());
if (args instanceof LiteralArgumentBuilder literal) {
helper.add("argsLabel", literal.getLiteral());
} else if (args instanceof RequiredArgumentBuilder required) {
helper.add("argsName", required.getName());
}
}
@@ -348,18 +345,20 @@ public class AvailableCommandsPacket implements MinecraftPacket {
* A placeholder {@link SuggestionProvider} used internally to preserve the suggestion provider
* name.
*/
public static class ProtocolSuggestionProvider implements SuggestionProvider<CommandSource> {
private final String name;
public ProtocolSuggestionProvider(String name) {
this.name = name;
}
public record ProtocolSuggestionProvider(String name) implements SuggestionProvider<CommandSource> {
@Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSource> context,
SuggestionsBuilder builder) throws CommandSyntaxException {
SuggestionsBuilder builder) {
return builder.buildFuture();
}
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// This is a very complex packet to encode. Paper 1.21.10 + Velocity with Spark has a size of
// 30,334, but this is likely on the lower side. We'll use 128KiB as a more realistically-sized
// amount.
return 128 * 1024;
}
}
@@ -207,30 +207,22 @@ public class BossBarPacket implements MinecraftPacket {
this.uuid = ProtocolUtils.readUuid(buf);
this.action = ProtocolUtils.readVarInt(buf);
switch (action) {
case ADD:
case ADD -> {
this.name = ComponentHolder.read(buf, version);
this.percent = buf.readFloat();
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
this.flags = buf.readUnsignedByte();
break;
case REMOVE:
break;
case UPDATE_PERCENT:
this.percent = buf.readFloat();
break;
case UPDATE_NAME:
this.name = ComponentHolder.read(buf, version);
break;
case UPDATE_STYLE:
}
case REMOVE -> {}
case UPDATE_PERCENT -> this.percent = buf.readFloat();
case UPDATE_NAME -> this.name = ComponentHolder.read(buf, version);
case UPDATE_STYLE -> {
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
break;
case UPDATE_PROPERTIES:
this.flags = buf.readUnsignedByte();
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
case UPDATE_PROPERTIES -> this.flags = buf.readUnsignedByte();
default -> throw new UnsupportedOperationException("Unknown action " + action);
}
}
@@ -242,36 +234,30 @@ public class BossBarPacket implements MinecraftPacket {
ProtocolUtils.writeUuid(buf, uuid);
ProtocolUtils.writeVarInt(buf, action);
switch (action) {
case ADD:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
name.write(buf);
buf.writeFloat(percent);
case ADD -> {
if (name == null) {
throw new IllegalStateException("No name specified!");
}
name.write(buf);
buf.writeFloat(percent);
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags);
}
case REMOVE -> {}
case UPDATE_PERCENT -> buf.writeFloat(percent);
case UPDATE_NAME -> {
if (name == null) {
throw new IllegalStateException("No name specified!");
}
name.write(buf);
}
case UPDATE_STYLE -> {
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags);
break;
case REMOVE:
break;
case UPDATE_PERCENT:
buf.writeFloat(percent);
break;
case UPDATE_NAME:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
name.write(buf);
break;
case UPDATE_STYLE:
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
break;
case UPDATE_PROPERTIES:
buf.writeByte(flags);
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
case UPDATE_PROPERTIES -> buf.writeByte(flags);
default -> throw new UnsupportedOperationException("Unknown action " + action);
}
}
@@ -22,8 +22,8 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.Nullable;
public class ClientSettingsPacket implements MinecraftPacket {
@@ -135,7 +135,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance +
", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" +
skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + textFilteringEnabled +
", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}';
", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}';
}
@Override
@@ -206,6 +206,16 @@ public class ClientSettingsPacket implements MinecraftPacket {
return handler.handle(this);
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 1 + ByteBufUtil.utf8MaxBytes(16) + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
}
@Override
public boolean equals(@Nullable final Object o) {
if (this == o) {
@@ -237,7 +247,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
difficulty,
skinParts,
mainHand,
textFilteringEnabled,
textFilteringEnabled,
clientListingAllowed,
particleStatus);
}
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2025 Velocity 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 com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.sound.Sound;
import org.jetbrains.annotations.Nullable;
import java.util.Random;
public class ClientboundSoundEntityPacket implements MinecraftPacket {
private static final Random SEEDS_RANDOM = new Random();
private Sound sound;
private @Nullable Float fixedRange;
private int emitterEntityId;
public ClientboundSoundEntityPacket() {}
public ClientboundSoundEntityPacket(Sound sound, @Nullable Float fixedRange, int emitterEntityId) {
this.sound = sound;
this.fixedRange = fixedRange;
this.emitterEntityId = emitterEntityId;
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Decode is not implemented");
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
ProtocolUtils.writeVarInt(buf, 0); // version-dependent, hardcoded sound ID
ProtocolUtils.writeMinimalKey(buf, sound.name());
buf.writeBoolean(fixedRange != null);
if (fixedRange != null)
buf.writeFloat(fixedRange);
ProtocolUtils.writeSoundSource(buf, protocolVersion, sound.source());
ProtocolUtils.writeVarInt(buf, emitterEntityId);
buf.writeFloat(sound.volume());
buf.writeFloat(sound.pitch());
buf.writeLong(sound.seed().orElse(SEEDS_RANDOM.nextLong()));
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
public Sound getSound() {
return sound;
}
public void setSound(Sound sound) {
this.sound = sound;
}
public @Nullable Float getFixedRange() {
return fixedRange;
}
public void setFixedRange(@Nullable Float fixedRange) {
this.fixedRange = fixedRange;
}
public int getEmitterEntityId() {
return emitterEntityId;
}
public void setEmitterEntityId(int emitterEntityId) {
this.emitterEntityId = emitterEntityId;
}
}
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2025 Velocity 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 com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop;
import javax.annotation.Nullable;
public class ClientboundStopSoundPacket implements MinecraftPacket {
private @Nullable Sound.Source source;
private @Nullable Key soundName;
public ClientboundStopSoundPacket() {}
public ClientboundStopSoundPacket(SoundStop soundStop) {
this(soundStop.source(), soundStop.sound());
}
public ClientboundStopSoundPacket(@Nullable Sound.Source source, @Nullable Key soundName) {
this.source = source;
this.soundName = soundName;
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int flagsBitmask = buf.readByte();
if ((flagsBitmask & 1) != 0) {
source = ProtocolUtils.readSoundSource(buf, protocolVersion);
} else {
source = null;
}
if ((flagsBitmask & 2) != 0) {
soundName = ProtocolUtils.readKey(buf);
} else {
soundName = null;
}
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int flagsBitmask = 0;
if (source != null && soundName == null) {
flagsBitmask |= 1;
} else if (soundName != null && source == null) {
flagsBitmask |= 2;
} else if (source != null /*&& sound != null*/) {
flagsBitmask |= 3;
}
buf.writeByte(flagsBitmask);
if (source != null) {
ProtocolUtils.writeSoundSource(buf, protocolVersion, source);
}
if (soundName != null) {
ProtocolUtils.writeMinimalKey(buf, soundName);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Nullable
public Sound.Source getSource() {
return source;
}
public void setSource(@Nullable Sound.Source source) {
this.source = source;
}
@Nullable
public Key getSoundName() {
return soundName;
}
public void setSoundName(@Nullable Key soundName) {
this.soundName = soundName;
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018-2025 Velocity 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 com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf;
public class DialogClearPacket implements MinecraftPacket {
public static final DialogClearPacket INSTANCE = new DialogClearPacket();
private DialogClearPacket() {
}
@Override
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
}
@Override
public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2018-2025 Velocity 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 com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.StateRegistry;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.BinaryTagIO;
public class DialogShowPacket implements MinecraftPacket {
private final StateRegistry state;
private int id;
private BinaryTag nbt;
public DialogShowPacket(final StateRegistry state) {
this.state = state;
}
@Override
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
this.id = this.state == StateRegistry.CONFIG ? 0 : ProtocolUtils.readVarInt(buf);
if (this.id == 0) {
this.nbt = ProtocolUtils.readBinaryTag(buf, protocolVersion, BinaryTagIO.reader());
}
}
@Override
public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
if (this.state == StateRegistry.CONFIG) {
ProtocolUtils.writeBinaryTag(buf, protocolVersion, this.nbt);
} else {
ProtocolUtils.writeVarInt(buf, this.id);
if (this.id == 0) {
ProtocolUtils.writeBinaryTag(buf, protocolVersion, this.nbt);
}
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -107,7 +107,7 @@ public class EncryptionResponsePacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
// It turns out these come out to the same length, whether we're talking >=1.8 or not.
// The length prefix always winds up being 2 bytes.
int base = 256 + 2 + 2;
@@ -123,8 +123,8 @@ public class EncryptionResponsePacket implements MinecraftPacket {
}
@Override
public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
int base = expectedMaxLength(buf, direction, version);
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
int base = decodeExpectedMaxLength(buf, direction, version);
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
// These are "optional"
base -= 128 + 8;
@@ -24,6 +24,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf;
public class HandshakePacket implements MinecraftPacket {
@@ -108,14 +109,21 @@ public class HandshakePacket implements MinecraftPacket {
}
@Override
public int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 7;
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// We could compute an exact size, but 4KiB ought to be enough to encode all reasonable
// sizes of this packet.
return 4 * 1024;
}
}
@@ -64,6 +64,28 @@ public class KeepAlivePacket implements MinecraftPacket {
}
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_2)) {
return Long.BYTES;
} else if (version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
return 5;
} else {
return Integer.BYTES;
}
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_2)) {
return Long.BYTES;
} else if (version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
return 1;
} else {
return Integer.BYTES;
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@@ -69,33 +69,25 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
Item item = new Item(ProtocolUtils.readUuid(buf));
items.add(item);
switch (action) {
case ADD_PLAYER:
case ADD_PLAYER -> {
item.setName(ProtocolUtils.readString(buf));
item.setProperties(ProtocolUtils.readProperties(buf));
item.setGameMode(ProtocolUtils.readVarInt(buf));
item.setLatency(ProtocolUtils.readVarInt(buf));
item.setDisplayName(readOptionalComponent(buf, version));
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
if (buf.readBoolean()) {
item.setPlayerKey(ProtocolUtils.readPlayerKey(version, buf));
}
if (buf.readBoolean()) {
item.setPlayerKey(ProtocolUtils.readPlayerKey(version, buf));
}
}
break;
case UPDATE_GAMEMODE:
item.setGameMode(ProtocolUtils.readVarInt(buf));
break;
case UPDATE_LATENCY:
item.setLatency(ProtocolUtils.readVarInt(buf));
break;
case UPDATE_DISPLAY_NAME:
item.setDisplayName(readOptionalComponent(buf, version));
break;
case REMOVE_PLAYER:
//Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
case UPDATE_GAMEMODE -> item.setGameMode(ProtocolUtils.readVarInt(buf));
case UPDATE_LATENCY -> item.setLatency(ProtocolUtils.readVarInt(buf));
case UPDATE_DISPLAY_NAME -> item.setDisplayName(readOptionalComponent(buf, version));
case REMOVE_PLAYER -> {
//Do nothing, all that is needed is the uuid
}
default -> throw new UnsupportedOperationException("Unknown action " + action);
}
}
} else {
@@ -126,39 +118,32 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
ProtocolUtils.writeUuid(buf, uuid);
switch (action) {
case ADD_PLAYER:
case ADD_PLAYER -> {
ProtocolUtils.writeString(buf, item.getName());
ProtocolUtils.writeProperties(buf, item.getProperties());
ProtocolUtils.writeVarInt(buf, item.getGameMode());
ProtocolUtils.writeVarInt(buf, item.getLatency());
writeDisplayName(buf, item.getDisplayName(), version);
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
if (item.getPlayerKey() != null) {
buf.writeBoolean(true);
ProtocolUtils.writePlayerKey(buf, item.getPlayerKey());
} else {
buf.writeBoolean(false);
}
if (item.getPlayerKey() != null) {
buf.writeBoolean(true);
ProtocolUtils.writePlayerKey(buf, item.getPlayerKey());
} else {
buf.writeBoolean(false);
}
}
break;
case UPDATE_GAMEMODE:
ProtocolUtils.writeVarInt(buf, item.getGameMode());
break;
case UPDATE_LATENCY:
ProtocolUtils.writeVarInt(buf, item.getLatency());
break;
case UPDATE_DISPLAY_NAME:
writeDisplayName(buf, item.getDisplayName(), version);
break;
case REMOVE_PLAYER:
}
case UPDATE_GAMEMODE -> ProtocolUtils.writeVarInt(buf, item.getGameMode());
case UPDATE_LATENCY -> ProtocolUtils.writeVarInt(buf, item.getLatency());
case UPDATE_DISPLAY_NAME -> writeDisplayName(buf, item.getDisplayName(), version);
case REMOVE_PLAYER -> {
// Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
}
default -> throw new UnsupportedOperationException("Unknown action " + action);
}
}
} else {
Item item = items.get(0);
Item item = items.getFirst();
Component displayNameComponent = item.getDisplayName();
if (displayNameComponent != null) {
String displayName = LegacyComponentSerializer.legacySection()
@@ -269,7 +254,7 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
return this;
}
public IdentifiedKey getPlayerKey() {
public @Nullable IdentifiedKey getPlayerKey() {
return playerKey;
}
}
@@ -36,7 +36,7 @@ public class LoginAcknowledgedPacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 0;
}
@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -86,4 +87,9 @@ public class LoginPluginMessagePacket extends DeferredByteBufHolder implements M
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}
@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -88,4 +89,9 @@ public class LoginPluginResponsePacket extends DeferredByteBufHolder implements
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}
@@ -42,6 +42,16 @@ public class PingIdentifyPacket implements MinecraftPacket {
buf.writeInt(id);
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return Integer.BYTES;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return Integer.BYTES;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -30,6 +31,9 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class PluginMessagePacket extends DeferredByteBufHolder implements MinecraftPacket {
private static final int MAX_PAYLOAD_SIZE_CLIENTBOUND = getPayloadLimit(Direction.CLIENTBOUND);
private static final int MAX_PAYLOAD_SIZE_SERVERBOUND = getPayloadLimit(Direction.SERVERBOUND);
private @Nullable String channel;
public PluginMessagePacket() {
@@ -49,6 +53,19 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
return channel;
}
private static int getPayloadLimit(Direction direction) {
if (System.getProperty("velocity.max-plugin-message-payload-size") != null) {
return Integer.getInteger("velocity.max-plugin-message-payload-size");
}
if (direction == Direction.SERVERBOUND) {
return Integer.getInteger("velocity.max-plugin-message-payload-size.serverbound", 32767);
} else {
// This is the vanilla expected limit, a payload this large feels like a nightmare given the trust
// we give to servers...
return Integer.getInteger("velocity.max-plugin-message-payload-size.clientbound", 1048576);
}
}
public void setChannel(String channel) {
this.channel = channel;
}
@@ -99,6 +116,17 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES +
(direction == Direction.CLIENTBOUND ? MAX_PAYLOAD_SIZE_CLIENTBOUND : MAX_PAYLOAD_SIZE_SERVERBOUND);
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 1 + 0 + 0;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@@ -143,4 +171,9 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
public PluginMessagePacket touch(Object hint) {
return (PluginMessagePacket) super.touch(hint);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}
@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.protocol.packet;
import com.google.common.collect.Lists;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
@@ -51,7 +50,7 @@ public class RemovePlayerInfoPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) {
int length = ProtocolUtils.readVarInt(buf);
Collection<UUID> profilesToRemove = Lists.newArrayListWithCapacity(length);
Collection<UUID> profilesToRemove = ProtocolUtils.newList(length);
for (int idx = 0; idx < length; idx++) {
profilesToRemove.add(ProtocolUtils.readUuid(buf));
}
@@ -80,6 +80,26 @@ public class ResourcePackResponsePacket implements MinecraftPacket {
ProtocolUtils.writeVarInt(buf, status.ordinal());
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
return Long.BYTES * 2 + 1;
} else if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_9_4)) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + 1;
}
return 1;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
return Long.BYTES * 2 + 1;
} else if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_9_4)) {
return 1 + 0 + 1;
}
return 1;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
@@ -93,4 +113,4 @@ public class ResourcePackResponsePacket implements MinecraftPacket {
", status=" + status +
'}';
}
}
}
@@ -22,6 +22,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.Nullable;
@@ -121,4 +122,9 @@ public class ServerDataPacket implements MinecraftPacket {
public void setSecureChatEnforced(boolean secureChatEnforced) {
this.secureChatEnforced = secureChatEnforced;
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return 8 * 1024;
}
}
@@ -150,7 +150,7 @@ public class ServerLoginPacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
// Accommodate the rare (but likely malicious) use of UTF-8 usernames, since it is technically
// legal on the protocol level.
int base = 1 + (16 * 3);
@@ -23,6 +23,7 @@ import com.velocitypowered.api.util.UuidUtils;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.util.VelocityProperties;
import io.netty.buffer.ByteBuf;
import java.util.List;
@@ -132,4 +133,11 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// We could compute an exact size, but 4KiB ought to be enough to encode all reasonable
// sizes of this packet.
return 4 * 1024;
}
}
@@ -65,6 +65,16 @@ public class ServerboundCookieResponsePacket implements MinecraftPacket {
}
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + 1 + 2 + 5120;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 1 + 0 + 0;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);

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