Compare commits

...
219 Commits
Author SHA1 Message Date
YoyoNow a52f338383 Merge pull request 'Update to 26.3' (#4) from update into master
SteamWarCI Build successful
Reviewed-on: #4
2026-09-24 17:09:44 +02:00
yoyosource 7c79516567 Fix compile of StateRegistry
SteamWarCI Build successful
2026-09-24 17:08:41 +02:00
yoyosource f4eb5c7536 Fix UpdateTeamsPacket in StateRegistry
SteamWarCI Build failed
2026-09-24 17:07:17 +02:00
yoyosource 664826fec3 Merge remote-tracking branch 'upstream/dev/4.0.0' into update
SteamWarCI Build failed
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2026-09-24 17:05:06 +02:00
Rocco fb0f21f425 Update online mode in reconfigure join packet (#1889) 2026-09-23 14:09:07 +01:00
MiniDigger | Martin 5bb1992014 [ci skip] update namespace badge 2026-09-22 16:09:28 +02:00
MiniDigger | Martin f975230f14 [ci skip] migrate CI to namespace 🚀 (#1888) 2026-09-20 10:23:18 +02:00
Wouter Gritter a6f9de9581 Bound IntervalledCounter size under packet floods
The counter stored one entry per addTime call, so a client sending tiny
(even empty) packets could grow it without limit while staying under the
bytes-per-second limit, eventually OOMing the proxy. Merge data points
within 1ms of the newest one so the entry count is bounded by the window
size (~7000 for the default 7s window) rather than by packet rate.
2026-09-16 13:41:57 +02:00
Wouter Gritter 5000227d11 Snapshot 4.2.1-SNAPSHOT 2026-09-16 13:41:50 +02:00
Wouter Gritter c10b492554 Release 4.2.0 2026-09-14 18:36:05 +02:00
Aaron dff94da2b3 Minecraft 26.3 (#1867)
* 26.3-snapshot-7

* 26.3-snapshot-8

* 26.3-snapshot-9

* 26.3-snapshot-10

* 26.3-pre-1

* 26.3-pre-2

* feat: add post effects packet

* chore: write game modes as var int

* 26.3-pre-3

* 26.3-rc-1

* 26.3

* chore: add key array cap

* fix: add a zero
2026-09-14 18:34:59 +02:00
Braydon 1b48f40534 Fix config reload not removing deleted servers (#1882)
* Fix config reload not removing deleted servers

When reloading velocity.toml, unregister servers that were removed from
the config and evacuate any connected players to fallback servers.

* Requested PR fixes

* Make Expected ConnectedPlayer exceptions more detailed
2026-09-14 18:34:50 +02:00
Wouter Gritter bc55300123 Bump com.puppycrawl.tools:checkstyle to 14.1.0 and fix warnings that were false-negatives before [ci skip]
Checkstyle fixed a bunch of false-negatives; linting errors in the source that should've been caught according to our settings. Rather than updating `checkstyle.xml` to work around these, this commit also properly fixes these checkstyle errors.
2026-09-13 14:01:39 +02:00
Wouter Gritter 15b6ddd1bf Various dependency bumps 2026-09-13 12:41:56 +02:00
21bbf35d1d Adding more options for configuring ping passthrough. (#1870)
* Added 'ALLBUTVERSION' option for ping passthrough.

* Added more configuration options for ping passthrough.

* Updated default velocity.toml

* Add support for the legacy ping passthrough.

* Removed legacy ping passthrough & added config migration

- Bumped config-version to 2.8
- Updated comments and grammar
- Removed legacy ping passthrough in the config

* Cleaned up code to match code style

* Update to latest

* Move ping passthrough into its own section

* Reorder migrations so they occur in the right order.

* Clean up ping passthrough related code.

* Update file dates and cleanup/consolidate ping-passthrough code.

* Update proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java

* Update proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java

* Remaining nits

- Read a string instead of LegacyPingPassthroughMode
- Update `announce-forge`'s comment in `PingPassthroughMigration`
- Consume `Config` instead of `CommentedConfig` in `PingPassthroughMode#fromConfig`

---------

Co-authored-by: TheMiningTeamYT <loanisamazing@outlook.com>
Co-authored-by: ButterDebugger <34288129+ButterDebugger@users.noreply.github.com>
Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-09-01 14:49:24 +02:00
Wouter Gritter e84b48a716 Clean up Title packets and enforce action types (#1803) 2026-09-01 13:55:10 +02:00
Wouter Gritter 940f395e3e Use correct minimum ClientSettings packet length for older protocol versions
For correctness's sake: in practice this bug never got triggered because `locale` is almost always a string of length 5+ ("en_us").
2026-09-01 13:19:46 +02:00
Wouter Gritter ddea763c8b Snapshot 4.1.2-SNAPSHOT 2026-09-01 13:10:58 +02:00
yoyosource a3b6177059 Trigger rebuild
SteamWarCI Build successful
2026-08-27 08:31:23 +02:00
yoyosource 3ef5b2ab99 Trigger rebuild
SteamWarCI Build successful
2026-08-27 08:25:34 +02:00
yoyosource 7a8bf8a5e0 Trigger rebuild
SteamWarCI Build failed
2026-08-27 08:22:06 +02:00
yoyosource 05de745780 Trigger rebuild
SteamWarCI Build successful
2026-08-27 08:19:47 +02:00
yoyosource 75ce1a9bf7 Fix StateRegistry maybe
SteamWarCI Build successful
2026-08-27 08:12:17 +02:00
Shane Freeder db0a17eea6 Velocity 4.1.1 release 2026-08-26 20:29:34 +01:00
Wouter Gritter 133f0e36b1 Switch to JLine's jni backend
FFM is the canonical choice for Java 22+, but it has some issues (mainly crashing the JVM on window resize), see jline/jline3#2139 and jline/jline3#2141

Downgrading to 4.0.x isn't an option as it re-introduces CVE-2026-77420..77423

Once the referenced PR gets merged and released, jline should be bumped to jline-terminal-ffm's latest version again
2026-08-26 12:09:46 +02:00
Wouter Gritter b2ba2e66bf Snapshot 4.1.1-SNAPSHOT 2026-08-26 12:09:07 +02:00
Shane Freeder a078053e26 velocity 4.1.0 release 2026-08-24 17:26:12 +01:00
yoyosource 7a39ebb058 Fix StateRegistry and update to dev/4.0.0
SteamWarCI Build successful
2026-08-24 17:14:49 +02:00
yoyosource 57e9cd7f24 Merge remote-tracking branch 'upstream/dev/4.0.0'
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
2026-08-24 17:03:57 +02:00
Wouter Gritter 4772ca3022 Bump lmbda to 3.0.0, defining generated handlers with the proxy's lookup instead of the plugin's
Reapplies the lmbda 3.0.0 bump reverted in f918d0d6.
2026-08-14 12:53:49 +02:00
YoyoNow b9463125d1 Merge pull request 'Update26' (#3) from update26 into master
SteamWarCI Build successful
Reviewed-on: #3
2026-08-13 09:23:04 +02:00
Shane Freeder f918d0d649 Downgrade lmbda back to 2.0.0
lmbda 3.x moved to using hidden classes for generation which does
not work for cross classloader operations which are somewhat typical
for plugins.
2026-08-12 23:39:19 +01:00
Wouter Gritter e6fbcc9196 Various dependency bumps (#1860)
* Bump fastutil to 8.5.19 and remove exclusions

* Various dependency bumps
2026-08-12 18:32:24 +01:00
Radmir Noirusov 14a69904f9 fix: retain reference-counted packets forwarded via handleGeneric (#1856)
During configuration, a ServerboundCustomClickActionPacket arriving
when connectionInFlight is null falls through to handleGeneric, which
writes it to the connected backend without retaining. The encoder
releases the packet, then MinecraftConnection.channelRead's finally
block releases again - double-free.

Two fixes:
- handle() now uses getConnectionInFlightOrConnectedServer() so the
  packet is properly retained before being written
- handleGeneric() retains any ByteBufHolder packet before write, not
  just PluginMessagePacket

Closes #1841
2026-08-12 18:29:30 +01:00
Jason Penilla 71c50a75eb Export JSpecify annotations at runtime (#1861)
JSpecify annotations have runtime retention, so expose them through the API variant as recommended by JSpecify.
2026-08-12 18:25:15 +01:00
Shane Freeder 00759e5279 Revert "Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)"
This reverts commit 2676520c6a.
2026-08-03 14:09:05 +01:00
Phillipp W. 06ade4775e fix: forward player loaded packet to backend server (#1862)
Return false after handling ServerboundPlayerLoadedPacket so Velocity's normal forwarding path sends the packet to the backend server.
2026-08-03 12:22:58 +01:00
Wouter Gritterandxphorror 2676520c6a Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)
Rewrite the comment and ternary operation to be clearer

Co-authored-by: xphorror <87706197+xphorror@users.noreply.github.com>
2026-08-02 11:13:25 +02:00
e11584ba35 Player Loaded World API (#1541)
Co-authored-by: Emil <12966472+Emilxyz@users.noreply.github.com>
Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-07-31 20:03:22 +01:00
Shane Freeder a08972749b Rebuild natives 2026-07-30 16:51:07 +01:00
AuriandShane Freeder c6e9ca989e Add provides API (#1853)
Add provides API

Co-authored-by: Shane Freeder <theboyetronic@gmail.com>
2026-07-29 12:51:34 +01:00
Shane Freeder d30f1d9a74 Compressor cleanups 2026-07-29 00:51:41 +01:00
Emil b45716deff feat: Make version clickable in velocity info command (#1775) 2026-07-19 16:02:20 +02:00
SpigotRCE e653647962 [ci skip] typo fix (#1778) 2026-07-19 16:02:12 +02:00
Shane Freeder 5aab0d1427 [ci skip] primative and functional
oh, my! This stuff is not ideal, but it's the only real protection here unless
we just hack the automation here
2026-07-15 04:28:22 +01:00
Shane Freeder 1cd8d51d02 [ci skip] primative failsafe for mismatched version family 2026-07-15 02:29:29 +01:00
Shane Freeder 9eb338bd1c Fix version family 2026-07-15 02:22:41 +01:00
Andrew Steinborn 60380211f8 Defer ByteBufUtil.getBytes() in config stages until after we check for channel registry 2026-07-14 18:04:07 -04:00
Shane Freeder 3b239daf4c [ci skip] back to snapshots
Hello, Darkness, my old friend
2026-07-14 15:26:19 +01:00
Shane Freeder 90f89053a7 Release 4.0.0 2026-07-14 15:10:04 +01:00
Chaoscaot 67e16a308e Merge remote-tracking branch 'upstream/dev/4.0.0'
SteamWarCI Build successful
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
2026-07-13 21:48:01 +02:00
Shane Freeder 4bd30bac33 Bump netty to 4.2.16.Final 2026-07-11 22:06:09 +01:00
Beanes 28c9f5a356 Small optimization to prevent blocking netty threads on UUID.randomUUID() (#1781)
* Small optimization to prevent blocking netty threads on UUID.randomUUID()

* Change FastRandomUuid to be a valid uuid v4

* Update javadoc for spotless

* Migrate method to VelocityTabListLegacy and add notice that it is insecure

* Bring back deleted override
2026-07-11 22:09:17 +02:00
Wouter Gritter da7427fb51 Set TitleActionbarPacket's default action to SET_ACTION_BAR (#1802) 2026-07-11 22:09:11 +02:00
Wouter Gritter b8d1f16b07 Setup encryption before possible disconnect (#1754) 2026-07-11 22:09:03 +02:00
Wouter Gritter 729a050662 Fix build-time GraalVmProcessor warning (#1801) 2026-07-11 22:08:55 +02:00
Wouter Gritter c690b4abfc Strip pre-java-9 version check in Metrics as Velocity targets 21+ (#1836)
* Strip pre-java-9 version check in `Metrics` as Velocity targets 21+

* Simplify Java version check by using `Runtime.version()` (entry still needs the system property to produce the same bStats metrics)

* Reconstruct `java.version` system property through `Runtime.Version`

* Update `javaVersion()` javadoc

* Newline while we're here
2026-07-11 22:08:47 +02:00
Wouter Gritter 5cc6b0b7e5 Use SecureRandom to generate verification/anti-MITM token (#1797) 2026-07-11 22:08:36 +02:00
Wouter Gritter 757711a5bc Use correct max tab complete lengths for < 1.13, = 1.13 and > 1.13 (#1796) 2026-07-11 22:08:30 +02:00
Shane Freeder 95694cba28 Update jline to 4.3.1 2026-07-11 20:17:21 +01:00
Wouter Gritter 7cb01b85f0 [ci skip] Bump API source & docs link to java 25 2026-07-11 20:54:54 +02:00
Shane Freeder 0942e162c0 bump version to 4.0.0-SNAPSHOT 2026-07-11 19:06:13 +01:00
Shane Freeder c1cd71a4bc Toolchain and gradle bump (Java 25+) 2026-07-11 19:05:05 +01:00
Shane Freeder 843a47e2a3 [ci skip] Merge branch 'rel/3.0.0' into dev/3.0.0 2026-07-11 19:00:03 +01:00
Shane Freeder 4498f1e03d Release 3.5.1
Bumps fill plugin to mitigate a deployment issue
2026-07-11 16:56:22 +01:00
Shane Freeder 1abb3ba56a Release 3.5.0 2026-07-11 11:55:53 +01:00
Shane Freeder a5680fc223 chore: Bump dependencies in alignment with paper 2026-07-11 02:21:15 +01:00
Emil ffca982a9d chore: drop adventure-platform-facet (#1843) 2026-07-11 02:09:54 +01:00
Shane Freeder 2b5d964ab0 Reapply "Adventure 5 (#1774)"
This reverts commit 06eb052ab5.
2026-07-11 01:36:17 +01:00
Shane Freeder f11cc86e69 Welcome to the 3.6.0 series 2026-07-11 01:36:06 +01:00
Shane Freeder 06eb052ab5 Revert "Adventure 5 (#1774)"
This reverts commit 97b386d86f.
2026-07-11 01:33:41 +01:00
Adrian 97b386d86f Adventure 5 (#1774)
* Adventure 5.2.0
2026-07-10 11:53:08 +01:00
Clement Raynaud 81a5817a82 Resolve backend DNS on a bounded thread pool to avoid head-of-line blocking (#1834)
* Use a bounded thread pool for backend DNS resolution

* Queue DNS lookups instead of rejecting when the resolver pool is busy
2026-07-08 11:27:56 +02:00
Wouter Gritter 1edab1411d Store historical console commands in .console_history (enables command history between restarts) (#1835) 2026-07-05 16:10:22 +01:00
Shane Freeder a7581821fb Use a shared per-proxy session ID for 26.2 login metrics
The 26.2 login success session ID is purely a metrics identifier. Mint
one shared UUID per proxy, regenerated when the proxy empties, mirroring
the vanilla server, instead of a random UUID per connection.
2026-06-16 12:27:00 +01:00
Aaron d7ad0522e9 Minecraft 26.2 (#1807)
* 26.2-snapshot-2

* 26.2-snapshot-3

* 26.2-snapshot-4

* 26.2-pre-1

* 26.2-pre-3

* 26.2-pre-4

* chore: set online mode and session id

* fix: checkstyle

* 26.2-pre-5

* 26.2-pre-6

* 26.2-rc-1

* 26.2-rc-2

* unregister old color argument in 26.2

* 26.2
2026-06-16 12:20:59 +01:00
JonesandWouter Gritter 0cbe10e951 Bump netty from 4.2.10.Final to 4.2.15.Final (#1817)
* Bump netty from 4.2.10.Final to 4.2.14.Final

* Bump netty from 4.2.14.Final to 4.2.15.Final

Co-authored-by: Wouter Gritter <wouter@gritter.nl>

---------

Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-06-15 15:49:05 +01:00
Wouter Gritter bcf1bba3a1 Add missing package-info.javas for event packages (#1822) 2026-06-15 15:48:08 +01:00
Wouter Gritter 3b892899db Require non-null reason (#1823) 2026-06-15 15:47:31 +01:00
Wouter Gritter 1b64a70a00 Fix underflow and overflow error messages (#1812) 2026-06-02 16:22:33 +02:00
GoldenEdit 3b142f3099 Fix inverted PluginMessageEvent source/target in InitialConnectSessionHandler (#1811) 2026-05-28 11:02:48 +01:00
Wouter Gritter 25fbd833cd Add decompressed-bytes-per-second rate limit, update packet limiter defaults (#1786)
* Add decompressed-bytes-per-second packet limiter, update defaults
* Revert "Add compression ratio limiter"
2026-05-24 16:59:34 +01:00
Wouter Gritter b72cf26802 Cap pre-join plugin-message queue size (prevents arbitrary growth/OOM) (#1800)
* Cap pre-join plugin-message queue size (prevents arbitrary growth/OOM)

* Clear counters once as the entire queue will have been processed
2026-05-24 16:55:07 +01:00
Wouter Gritter 7d68208e11 Always close HttpClient (#1798)
* Always close HttpClient (even on exception)

* Get rid of try/catch since we now use java 21
2026-05-22 10:25:48 -05:00
Wouter Gritter 38ff21afd7 Fix theoretical IOOBE race (#1799) 2026-05-22 15:50:48 +01:00
yoyosource 4b8ceb5264 Fix UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 09:53:29 +02:00
yoyosource 2d1cade7bd Fix UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 09:19:40 +02:00
yoyosource 4067c44cd1 Fix UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 09:06:32 +02:00
yoyosource 45bb5de601 Fix Packet ID in 1.21.9 for UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 08:52:49 +02:00
yoyosource ebb9eb046d Fix UpdateTeamsPacket for 1.21.5+
SteamWarCI Build successful
2026-05-21 08:35:30 +02:00
Chaoscaot afc84b88ac Fix
SteamWarCI Build successful
2026-05-21 08:30:09 +02:00
Chaoscaot e6caa87865 Fix
SteamWarCI Build failed
2026-05-21 08:26:58 +02:00
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
bookyandDwarslooper 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-codeandAdrian Gonzales 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
RossandRoss 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 SeidelandAdrian Gonzales 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
AaronandEmilxyz 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
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
EcconiaandAdrian Gonzales 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
JonesandNassim Jahnke 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
230 changed files with 5769 additions and 2025 deletions
+19 -9
View File
@@ -6,18 +6,28 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: namespace-profile-linux-default
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: namespacelabs/nscloud-checkout-action@v9
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Gradle - name: Set up cache
uses: gradle/actions/setup-gradle@v4 uses: namespacelabs/nscloud-cache-action@v1
- name: Set up JDK 17
uses: actions/setup-java@v4
with: with:
java-version: 17 cache: gradle
distribution: 'temurin' - name: Setup gradle build cache
run: |
nsc cache gradle setup --init-gradle /tmp/init.gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-disabled: true # namespace handles this
- name: Set up JDK 21
uses: actions/setup-java@v6
with:
java-version: 21
distribution: 'zulu'
cache: "" # namespace handles this
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew --init-script=/tmp/init.gradle build
+8
View File
@@ -34,3 +34,11 @@ and you can configure it from there.
Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity) Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity)
page. 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.
[![CI powered by namespace badge](https://papermc.io/assets/misc/namespace-oss-badge.svg?project=velocity)](https://namespace.so/github-actions/?utm_source=oss&utm_campaign=papermc)
+6 -8
View File
@@ -18,7 +18,7 @@ java {
} }
dependencies { dependencies {
compileOnlyApi(libs.jspecify) api(libs.jspecify)
api(libs.gson) api(libs.gson)
api(libs.guava) api(libs.guava)
@@ -59,18 +59,16 @@ tasks {
val o = options as StandardJavadocDocletOptions val o = options as StandardJavadocDocletOptions
o.encoding = "UTF-8" o.encoding = "UTF-8"
o.source = "17" o.source = "25"
o.use() o.use()
o.links( 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://guava.dev/releases/${libs.guava.get().version}/api/docs/",
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
"https://docs.oracle.com/en/java/javase/17/docs/api/", "https://docs.oracle.com/en/java/javase/25/docs/api/",
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/", "https://jd.papermc.io/adventure/${libs.adventure.bom.get().version}/",
"https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/", "https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.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}/",
) )
o.tags( o.tags(
@@ -14,7 +14,6 @@ import com.velocitypowered.api.plugin.Plugin;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.ProcessingEnvironment;
@@ -68,8 +67,8 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
Name qualifiedName = ((TypeElement) element).getQualifiedName(); Name qualifiedName = ((TypeElement) element).getQualifiedName();
if (Objects.equals(pluginClassFound, qualifiedName.toString())) { if (pluginClassFound != null) {
if (!warnedAboutMultiplePlugins) { if (!pluginClassFound.equals(qualifiedName.toString()) && !warnedAboutMultiplePlugins) {
environment.getMessager() environment.getMessager()
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support " .printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
+ "multiple plugins. We are using " + pluginClassFound + "multiple plugins. We are using " + pluginClassFound
@@ -98,6 +97,16 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
} }
} }
for (String provided : plugin.provides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(provided).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Invalid provided ID '" + provided + "' for plugin " + qualifiedName
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
+ "can contain dashes or underscores.");
return false;
}
}
// All good, generate the velocity-plugin.json. // All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription SerializedPluginDescription description = SerializedPluginDescription
.from(plugin, qualifiedName.toString()); .from(plugin, qualifiedName.toString());
@@ -24,7 +24,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/ */
public final class SerializedPluginDescription { 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 // @Nullable is used here to make GSON skip these in the serialized file
private final String id; private final String id;
@@ -34,11 +35,12 @@ public final class SerializedPluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final @Nullable List<String> authors; private final @Nullable List<String> authors;
private final @Nullable List<Dependency> dependencies; private final @Nullable List<Dependency> dependencies;
private final @Nullable List<String> provides;
private final String main; private final String main;
private SerializedPluginDescription(String id, String name, String version, String description, private SerializedPluginDescription(String id, String name, String version, String description,
String url, String url,
List<String> authors, List<Dependency> dependencies, String main) { List<String> authors, List<Dependency> dependencies, List<String> provides, String main) {
Preconditions.checkNotNull(id, "id"); Preconditions.checkNotNull(id, "id");
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid"); Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
this.id = id; this.id = id;
@@ -49,6 +51,7 @@ public final class SerializedPluginDescription {
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors; this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
this.dependencies = this.dependencies =
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies; dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides;
this.main = Preconditions.checkNotNull(main, "main"); this.main = Preconditions.checkNotNull(main, "main");
} }
@@ -60,7 +63,9 @@ public final class SerializedPluginDescription {
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
plugin.description(), plugin.url(), plugin.description(), plugin.url(),
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()) Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
.collect(Collectors.toList()), dependencies, qualifiedName); .collect(Collectors.toList()), dependencies,
Arrays.stream(plugin.provides()).filter(provided -> !provided.isEmpty())
.collect(Collectors.toList()), qualifiedName);
} }
public String getId() { public String getId() {
@@ -91,6 +96,10 @@ public final class SerializedPluginDescription {
return dependencies == null ? ImmutableList.of() : dependencies; return dependencies == null ? ImmutableList.of() : dependencies;
} }
public List<String> getProvides() {
return provides == null ? ImmutableList.of() : provides;
}
public String getMain() { public String getMain() {
return main; return main;
} }
@@ -111,12 +120,13 @@ public final class SerializedPluginDescription {
&& Objects.equals(url, that.url) && Objects.equals(url, that.url)
&& Objects.equals(authors, that.authors) && Objects.equals(authors, that.authors)
&& Objects.equals(dependencies, that.dependencies) && Objects.equals(dependencies, that.dependencies)
&& Objects.equals(provides, that.provides)
&& Objects.equals(main, that.main); && Objects.equals(main, that.main);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(id, name, version, description, url, authors, dependencies); return Objects.hash(id, name, version, description, url, authors, dependencies, provides);
} }
@Override @Override
@@ -129,6 +139,7 @@ public final class SerializedPluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", provides=" + provides
+ ", main='" + main + '\'' + ", main='" + main + '\''
+ '}'; + '}';
} }
@@ -23,7 +23,7 @@ public interface CommandSource extends Audience, PermissionSubject {
* Sends a message with the MiniMessage format to this source. * Sends a message with the MiniMessage format to this source.
* *
* @param message MiniMessage content * @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. * for more information on the format.
**/ **/
default void sendRichMessage(final @NotNull String message) { 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. * Sends a message with the MiniMessage format to this source.
* *
* @param message MiniMessage content * @param message MiniMessage content
* @param resolvers resolvers to use * @param resolvers resolvers to use
* @see <a href="https://docs.advntr.dev/minimessage/">MiniMessage docs</a> * @see <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a>
* and <a href="https://docs.advntr.dev/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a> * and <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
* for more information on the format. * for more information on the format.
**/ */
default void sendRichMessage( default void sendRichMessage(
final @NotNull String message, final @NotNull String message,
final @NotNull TagResolver @NotNull... resolvers final @NotNull TagResolver @NotNull... resolvers
@@ -60,7 +60,8 @@ public interface EventManager {
* *
* @param plugin the plugin to associate with the handler * @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register * @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 handler the handler to register
* @param <E> the event type to handle * @param <E> the event type to handle
*/ */
@@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 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.
*/
/**
* Provides events for handling command execution.
*/
package com.velocitypowered.api.event.command;
@@ -11,6 +11,7 @@ import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.event.annotation.AwaitingEvent; import com.velocitypowered.api.event.annotation.AwaitingEvent;
import com.velocitypowered.api.proxy.Player; 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. * 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> { public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
private final Player player; private final Player player;
private final String serverIdHash;
private ComponentResult result; private ComponentResult result;
@Deprecated(forRemoval = true)
public LoginEvent(Player player) { 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.player = Preconditions.checkNotNull(player, "player");
this.serverIdHash = serverIdHash;
this.result = ComponentResult.allowed(); this.result = ComponentResult.allowed();
} }
@@ -33,6 +48,16 @@ public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentRe
return player; 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 @Override
public ComponentResult getResult() { public ComponentResult getResult() {
return result; return result;
@@ -73,6 +73,7 @@ public final class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLogin
/** /**
* Returns the UUID of the connecting player. * Returns the UUID of the connecting player.
*
* <p>This value is {@code null} on 1.19.2 and lower, * <p>This value is {@code null} on 1.19.2 and lower,
* up to 1.20.1 it is optional and from 1.20.2 it will always be available.</p> * up to 1.20.1 it is optional and from 1.20.2 it will always be available.</p>
* *
@@ -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
+ '}';
}
}
@@ -40,9 +40,9 @@ public final class PlayerClientBrandEvent {
@Override @Override
public String toString() { public String toString() {
return "PlayerClientBrandEvent{" return "PlayerClientBrandEvent{"
+ "player=" + player + "player=" + player
+ ", brand='" + brand + '\'' + ", brand='" + brand + '\''
+ '}'; + '}';
} }
} }
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018-2026 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.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
/**
* Called when a player is marked as loaded by the client.
*
* <p>This event is fired once per {@link com.velocitypowered.api.proxy.ServerConnection}
* when the player explicitly notifies the server after loading the world (closing the downloading terrain screen)
*
* @implNote Unlike Paper this event will <u>not</u> fire due to a timeout nor respawning.
* Though plugins can implement a timeout by scheduling a task in {@link ServerPostConnectEvent}
* and checking {@link com.velocitypowered.api.proxy.ServerConnection#isClientLoaded()}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
public final class PlayerClientLoadedWorldEvent {
private final Player player;
public PlayerClientLoadedWorldEvent(Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}
public Player getPlayer() {
return player;
}
@Override
public String toString() {
return "PlayerClientLoadedWorldEvent{"
+ "player=" + player
+ '}';
}
}
@@ -143,7 +143,7 @@ public final class ServerPreConnectEvent implements
* is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status * is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status
* {@link Status#CONNECTION_CANCELLED}. * {@link Status#CONNECTION_CANCELLED}.
* *
* @return a result to deny conneections * @return a result to deny connections
*/ */
public static ServerResult denied() { public static ServerResult denied() {
return DENIED; return DENIED;
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player entered the configuration state and can be configured by Velocity. * This event is executed when a player entered the configuration state and can be configured by Velocity.
*
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p> * <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
* *
* @param player The player who can be configured. * @param player The player who can be configured.
@@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player is about to enter the configuration state. * This event is executed when a player is about to enter the configuration state.
* It is <b>not</b> called for the initial configuration of a player after login. * It is <b>not</b> called for the initial configuration of a player after login.
*
* <p>Velocity will wait for this event before asking the client to enter configuration state. * <p>Velocity will wait for this event before asking the client to enter configuration state.
* However due to backend server being unable to keep the connection alive during state changes, * However due to backend server being unable to keep the connection alive during state changes,
* Velocity will only wait for a maximum of 5 seconds.</p> * Velocity will only wait for a maximum of 5 seconds.</p>
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player has entered the configuration state. * This event is executed when a player has entered the configuration state.
*
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed, * <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
* the {@linkplain Player#getProtocolState()} method is guaranteed * the {@linkplain Player#getProtocolState()} method is guaranteed
* to return {@link ProtocolState#CONFIGURATION}.</p> * to return {@link ProtocolState#CONFIGURATION}.</p>
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player is about to finish the configuration state. * This event is executed when a player is about to finish the configuration state.
*
* <p>Velocity will wait for this event before asking the client to finish the configuration state. * <p>Velocity will wait for this event before asking the client to finish the configuration state.
* However due to backend server being unable to keep the connection alive during state changes, * However due to backend server being unable to keep the connection alive during state changes,
* Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration * Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player has finished the configuration state. * This event is executed when a player has finished the configuration state.
*
* <p>From this moment on, the {@link Player#getProtocolState()} method * <p>From this moment on, the {@link Player#getProtocolState()} method
* will return {@link ProtocolState#PLAY}.</p> * will return {@link ProtocolState#PLAY}.</p>
* *
@@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 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.
*/
/**
* Provides events for handling the player configuration phase.
*/
package com.velocitypowered.api.event.player.configuration;
@@ -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";
}
}
@@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 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.
*/
/**
* Provides events for handling registration of servers on the proxy.
*/
package com.velocitypowered.api.event.proxy.server;
@@ -16,6 +16,7 @@ package com.velocitypowered.api.network;
public enum ProtocolState { public enum ProtocolState {
/** /**
* Initial connection State. * Initial connection State.
*
* <p>This status can be caused by a {@link HandshakeIntent#STATUS}, * <p>This status can be caused by a {@link HandshakeIntent#STATUS},
* {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p> * {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p>
* If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN}, * If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN},
@@ -24,6 +25,7 @@ public enum ProtocolState {
HANDSHAKE, HANDSHAKE,
/** /**
* Ping State of a connection. * Ping State of a connection.
*
* <p>Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state * <p>Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state
* and be disconnected after it requests the ping from the server * and be disconnected after it requests the ping from the server
* and the server responds with the respective ping.</p> * and the server responds with the respective ping.</p>
@@ -31,11 +33,13 @@ public enum ProtocolState {
STATUS, STATUS,
/** /**
* Authentication State of a connection. * Authentication State of a connection.
*
* <p>At this moment the player is authenticating with the authentication servers.</p> * <p>At this moment the player is authenticating with the authentication servers.</p>
*/ */
LOGIN, LOGIN,
/** /**
* Configuration State of a connection. * Configuration State of a connection.
*
* <p>At this point the player allows the server to send information * <p>At this point the player allows the server to send information
* such as resource packs and plugin messages, at the same time the player * such as resource packs and plugin messages, at the same time the player
* will send his client brand and the respective plugin messages * will send his client brand and the respective plugin messages
@@ -46,6 +50,7 @@ public enum ProtocolState {
CONFIGURATION, CONFIGURATION,
/** /**
* Game State of a connection. * Game State of a connection.
*
* <p>In this state is where the whole game runs, the server is able to change * <p>In this state is where the whole game runs, the server is able to change
* the player's state to {@link #CONFIGURATION} as needed in versions 1.20.2 and higher.</p> * the player's state to {@link #CONFIGURATION} as needed in versions 1.20.2 and higher.</p>
*/ */
@@ -91,7 +91,13 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"), MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
MINECRAFT_1_21_4(769, "1.21.4"), MINECRAFT_1_21_4(769, "1.21.4"),
MINECRAFT_1_21_5(770, "1.21.5"), 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"),
MINECRAFT_26_2(776, "26.2"),
MINECRAFT_26_3(777, "26.3");
private static final int SNAPSHOT_BIT = 30; private static final int SNAPSHOT_BIT = 30;
@@ -15,7 +15,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* Represents three different states of a setting. * Represents three different states of a setting.
* *
* <p>Possible values:</p> * <p>Possible values:</p>
* <p></p>
* <ul> * <ul>
* <li>{@link #TRUE} - a positive setting</li> * <li>{@link #TRUE} - a positive setting</li>
* <li>{@link #FALSE} - a negative (negated) setting</li> * <li>{@link #FALSE} - a negative (negated) setting</li>
@@ -7,9 +7,11 @@
package com.velocitypowered.api.plugin; package com.velocitypowered.api.plugin;
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.intellij.lang.annotations.Pattern;
/** /**
* Indicates that the {@link Plugin} depends on another plugin in order to enable. * 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 * @return The dependency plugin ID
* @see Plugin#id() * @see Plugin#id()
*/ */
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id(); String id();
/** /**
@@ -7,10 +7,12 @@
package com.velocitypowered.api.plugin; package com.velocitypowered.api.plugin;
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.intellij.lang.annotations.Pattern;
/** /**
* Annotation used to describe a Velocity plugin. * Annotation used to describe a Velocity plugin.
@@ -26,6 +28,7 @@ public @interface Plugin {
* *
* @return the ID for this plugin * @return the ID for this plugin
*/ */
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id(); String id();
/** /**
@@ -69,4 +72,12 @@ public @interface Plugin {
* @return the plugin dependencies * @return the plugin dependencies
*/ */
Dependency[] dependencies() default {}; Dependency[] dependencies() default {};
/**
* The plugin IDs this plugin "provides" for. Each ID must match
* {@link SerializedPluginDescription#ID_PATTERN_STRING}.
*
* @return the provided IDs
*/
String[] provides() default {};
} }
@@ -100,6 +100,16 @@ public interface PluginDescription {
return Optional.empty(); return Optional.empty();
} }
/**
* Gets a {@link Collection} of the provided IDs of the {@link Plugin} within this container.
*
* @return the provided plugins IDs, can be empty
* @see Plugin#provides()
*/
default Collection<String> getProvidedIds() {
return ImmutableSet.of();
}
/** /**
* Returns the source the plugin was loaded from. * Returns the source the plugin was loaded from.
* *
@@ -29,6 +29,7 @@ import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import net.kyori.adventure.dialog.DialogLike;
import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.inventory.Book; import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.key.Key; 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.Component;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -48,7 +50,8 @@ public interface Player extends
/* Fundamental Velocity interfaces */ /* Fundamental Velocity interfaces */
CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink, CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink,
/* Adventure-specific interfaces */ /* 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. * Returns the player's current username.
@@ -194,7 +197,7 @@ public interface Player extends
* *
* @param reason component with the reason * @param reason component with the reason
*/ */
void disconnect(Component reason); void disconnect(@NotNull Component reason);
/** /**
* Sends chat input onto the players current server as if they typed it into the client chat box. * Sends chat input onto the players current server as if they typed it into the client chat box.
@@ -241,7 +244,7 @@ public interface Player extends
* Gets the {@link ResourcePackInfo} of the currently applied * Gets the {@link ResourcePackInfo} of the currently applied
* resource-pack or null if none. * resource-pack or null if none.
* *
* <p> Note that since 1.20.3 it is no longer recommended to use * <p>Note that since 1.20.3 it is no longer recommended to use
* this method as it will only return the last applied * this method as it will only return the last applied
* resource pack. To get all applied resource packs, use * resource pack. To get all applied resource packs, use
* {@link #getAppliedResourcePacks()} instead. </p> * {@link #getAppliedResourcePacks()} instead. </p>
@@ -257,7 +260,7 @@ public interface Player extends
* the user is currently downloading or is currently * the user is currently downloading or is currently
* prompted to install or null if none. * prompted to install or null if none.
* *
* <p> Note that since 1.20.3 it is no longer recommended to use * <p>Note that since 1.20.3 it is no longer recommended to use
* this method as it will only return the last pending * this method as it will only return the last pending
* resource pack. To get all pending resource packs, use * resource pack. To get all pending resource packs, use
* {@link #getPendingResourcePacks()} instead. </p> * {@link #getPendingResourcePacks()} instead. </p>
@@ -312,6 +315,7 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
*
* <p><strong>Note that this method does not send a plugin message to the server the player * <p><strong>Note that this method does not send a plugin message to the server the player
* is connected to.</strong> You should only use this method if you are trying to communicate * is connected to.</strong> You should only use this method if you are trying to communicate
* with a mod that is installed on the player's client.</p> * with a mod that is installed on the player's client.</p>
@@ -335,6 +339,15 @@ public interface Player extends
Component.text(getUsername())))); 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. * Gets the player's client brand.
@@ -383,8 +396,12 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@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 @Override
default void playSound(@NotNull Sound sound) { default void playSound(@NotNull Sound sound) {
@@ -393,8 +410,11 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* <b>This method is not currently implemented in Velocity * @apiNote <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b> * 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 @Override
default void playSound(@NotNull Sound sound, double x, double y, double z) { default void playSound(@NotNull Sound sound, double x, double y, double z) {
@@ -403,18 +423,28 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* <b>This method is not currently implemented in Velocity * <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+.
* and will not perform any actions.</b> *
* <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 @Override
default void playSound(@NotNull Sound sound, Sound.Emitter emitter) { default void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* <b>This method is not currently implemented in Velocity * @param stop the sound and/or a sound source, to stop
* and will not perform any actions.</b> * @since 3.4.0
* @sinceMinecraft 1.19.3
* @apiNote This method is currently only implemented for players on 1.19.3+.
*/ */
@Override @Override
default void stopSound(@NotNull SoundStop stop) { default void stopSound(@NotNull SoundStop stop) {
@@ -425,11 +455,40 @@ public interface Player extends
* *
* <b>This method is not currently implemented in Velocity * <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b> * 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 @Override
default void openBook(@NotNull Book book) { 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. * Transfers a Player to a host.
* *
@@ -7,6 +7,7 @@
package com.velocitypowered.api.proxy; package com.velocitypowered.api.proxy;
import com.google.common.annotations.Beta;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
*/ */
ServerInfo getServerInfo(); ServerInfo getServerInfo();
/**
* Returns whether the client notified this connection of having loaded the world.
*
* @return true if the client has loaded the world
* @implNote This is purely client-dependent; see {@link com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
boolean isClientLoaded();
/** /**
* Returns the player that this connection is associated with. * Returns the player that this connection is associated with.
* *
@@ -56,6 +56,7 @@ public interface KeySigned {
* Validates the signature, expiry temporal and key against the * Validates the signature, expiry temporal and key against the
* signer public key. Note: This will **not** check for * signer public key. Note: This will **not** check for
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}. * expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
*
* <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p> * <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p>
* Addendum: Does not work for 1.19.1 until the user has authenticated. * Addendum: Does not work for 1.19.1 until the user has authenticated.
* *
@@ -237,8 +237,11 @@ public interface TabListEntry extends KeyIdentifiable {
/** /**
* Sets the {@link IdentifiedKey} of the {@link TabListEntry}. * Sets the {@link IdentifiedKey} of the {@link TabListEntry}.
*
* <p>This only works for players currently <b>not</b> connected to this proxy.</p> * <p>This only works for players currently <b>not</b> connected to this proxy.</p>
*
* <p>For any player currently connected to this proxy this will be filled automatically.</p> * <p>For any player currently connected to this proxy this will be filled automatically.</p>
*
* <p>Will ignore mismatching key revisions data.</p> * <p>Will ignore mismatching key revisions data.</p>
* *
* @param chatSession session to set * @param chatSession session to set
@@ -19,7 +19,9 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; 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. * 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 Version version;
private final @Nullable Players players; 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 Favicon favicon;
private final @Nullable ModInfo modinfo; private final @Nullable ModInfo modinfo;
private final boolean preventsChatReports = true;
public ServerPing(Version version, @Nullable Players players, public ServerPing(Version version, @Nullable Players players,
net.kyori.adventure.text.Component description, @Nullable Favicon favicon) { net.kyori.adventure.text.Component description, @Nullable Favicon favicon) {
@@ -47,8 +50,8 @@ public final class ServerPing {
* @param modinfo the mods this server runs * @param modinfo the mods this server runs
*/ */
public ServerPing(Version version, @Nullable Players players, public ServerPing(Version version, @Nullable Players players,
net.kyori.adventure.text.Component description, @Nullable Favicon favicon, Component description, @Nullable Favicon favicon,
@Nullable ModInfo modinfo) { @Nullable ModInfo modinfo) {
this.version = Preconditions.checkNotNull(version, "version"); this.version = Preconditions.checkNotNull(version, "version");
this.players = players; this.players = players;
this.description = Preconditions.checkNotNull(description, "description"); this.description = Preconditions.checkNotNull(description, "description");
@@ -64,7 +67,8 @@ public final class ServerPing {
return Optional.ofNullable(players); return Optional.ofNullable(players);
} }
public net.kyori.adventure.text.Component getDescriptionComponent() { @Nullable
public Component getDescriptionComponent() {
return description; return description;
} }
@@ -151,7 +155,7 @@ public final class ServerPing {
private final List<SamplePlayer> samplePlayers = new ArrayList<>(); private final List<SamplePlayer> samplePlayers = new ArrayList<>();
private String modType = "FML"; private String modType = "FML";
private final List<ModInfo.Mod> mods = new ArrayList<>(); private final List<ModInfo.Mod> mods = new ArrayList<>();
private net.kyori.adventure.text.Component description; private Component description;
private @Nullable Favicon favicon; private @Nullable Favicon favicon;
private boolean nullOutPlayers; private boolean nullOutPlayers;
private boolean nullOutModinfo; private boolean nullOutModinfo;
@@ -299,7 +303,7 @@ public final class ServerPing {
* @param description Component to use as the description. * @param description Component to use as the description.
* @return this builder, for chaining * @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"); this.description = Preconditions.checkNotNull(description, "description");
return this; return this;
} }
@@ -359,7 +363,7 @@ public final class ServerPing {
return samplePlayers; return samplePlayers;
} }
public Optional<net.kyori.adventure.text.Component> getDescriptionComponent() { public Optional<Component> getDescriptionComponent() {
return Optional.ofNullable(description); return Optional.ofNullable(description);
} }
@@ -11,11 +11,14 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import java.util.UUID; 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. * 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 UUID id;
private final String undashedId; private final String undashedId;
@@ -169,6 +172,23 @@ public final class GameProfile {
ImmutableList.of()); 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 @Override
public String toString() { public String toString() {
return "GameProfile{" return "GameProfile{"
@@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import java.io.ByteArrayOutputStream 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 { val currentShortRevision = ByteArrayOutputStream().use {
exec { val execOps = objects.newInstance<Injected>().execOps
execOps.exec {
executable = "git" executable = "git"
args = listOf("rev-parse", "HEAD") args = listOf("rev-parse", "HEAD")
standardOutput = it standardOutput = it
@@ -25,5 +32,6 @@ tasks.withType<Jar> {
archiveVersion.get() archiveVersion.get()
} }
attributes["Implementation-Version"] = velocityHumanVersion attributes["Implementation-Version"] = velocityHumanVersion
attributes["Enable-Native-Access"] = "ALL-UNNAMED"
} }
} }
@@ -8,10 +8,10 @@ extensions.configure<PublishingExtension> {
maven { maven {
credentials(PasswordCredentials::class.java) credentials(PasswordCredentials::class.java)
name = "paper" name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined
val base = "https://repo.papermc.io/repository/maven" val base = "https://artifactory.papermc.io/artifactory"
val releasesRepoUrl = "$base-releases/" val releasesRepoUrl = "$base/releases/"
val snapshotsRepoUrl = "$base-snapshots/" val snapshotsRepoUrl = "$base/snapshots/"
setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
} }
} }
+6 -6
View File
@@ -12,7 +12,7 @@ subprojects {
java { java {
toolchain { toolchain {
languageVersion.set(JavaLanguageVersion.of(17)) languageVersion.set(JavaLanguageVersion.of(21))
} }
} }
@@ -20,11 +20,11 @@ subprojects {
testImplementation(rootProject.libs.junit) testImplementation(rootProject.libs.junit)
} }
tasks { testing.suites.named<JvmTestSuite>("test") {
test { useJUnitJupiter()
useJUnitPlatform() targets.all {
reports { testTask.configure {
junitXml.required.set(true) reports.junitXml.required = true
} }
} }
} }
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=3.4.0-SNAPSHOT version=4.2.1-SNAPSHOT
+28 -29
View File
@@ -1,27 +1,26 @@
[versions] [versions]
configurate3 = "3.7.3" configurate3 = "3.7.3"
configurate4 = "4.1.2" configurate4 = "4.2.0"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.24.3" log4j = "2.26.1"
netty = "4.2.1.Final" netty = "4.2.18.Final"
[plugins] [plugins]
indra-publishing = "net.kyori.indra.publishing:2.0.6" fill = "io.papermc.fill.gradle:1.0.12"
shadow = "io.github.goooler.shadow:8.1.5" shadow = "com.gradleup.shadow:9.6.1"
spotless = "com.diffplug.spotless:6.25.0" spotless = "com.diffplug.spotless:8.10.2"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:4.21.0" adventure-bom = "net.kyori:adventure-bom:5.2.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.21.0" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0"
adventure-facet = "net.kyori:adventure-platform-facet:4.3.4" asm = "org.ow2.asm:asm:9.10.1"
asm = "org.ow2.asm:asm:9.8" auto-service = "com.google.auto.service:auto-service:1.1.1"
auto-service = "com.google.auto.service:auto-service:1.0.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.0.3" bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
checker-qual = "org.checkerframework:checker-qual:3.42.0" checker-qual = "org.checkerframework:checker-qual:4.2.3"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" checkstyle = "com.puppycrawl.tools:checkstyle:14.1.0"
completablefutures = "com.spotify:completable-futures:0.3.6" completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" } configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" }
@@ -30,24 +29,24 @@ configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.r
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" } configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
disruptor = "com.lmax:disruptor:4.0.0" disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.15" fastutil = "it.unimi.dsi:fastutil:8.5.19"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", 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-jni:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:5.10.2" junit = "org.junit.jupiter:junit-jupiter:6.1.3"
jspecify = "org.jspecify:jspecify:0.3.0" jspecify = "org.jspecify:jspecify:1.0.1"
kyori-ansi = "net.kyori:ansi:1.1.1" kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:25.1-jre" guava = "com.google.guava:guava:33.7.1-jre"
gson = "com.google.code.gson:gson:2.10.1" gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:6.0.0" guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:2.0.0" lmbda = "org.lanternpowered:lmbda:3.0.0"
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } 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-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-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-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", 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.23.0"
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" } netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", 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" } netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
@@ -55,10 +54,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-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-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" } 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.9.0"
slf4j = "org.slf4j:slf4j-api:2.0.17" slf4j = "org.slf4j:slf4j-api:2.0.19"
snakeyaml = "org.yaml:snakeyaml:1.33" snakeyaml = "org.yaml:snakeyaml:2.7"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.4"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles] [bundles]
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+6 -10
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # 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 APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # 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 # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) 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. # 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: # 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. # 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 # * 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. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
@@ -249,4 +245,4 @@ eval "set -- $(
tr '\n' ' ' 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 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. @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 @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
@@ -70,11 +70,10 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @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 :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
@@ -91,4 +90,4 @@ exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
:omega :omega
@@ -5,29 +5,44 @@ set -e
# make sure we're in the correct directory - the top-level `native` directory # make sure we're in the correct directory - the top-level `native` directory
cd "$(dirname "$0")/.." || exit 1 cd "$(dirname "$0")/.." || exit 1
# Use docker by default, falling back to podman. Set CONTAINER_ENGINE to pick one explicitly.
if [ -z "$CONTAINER_ENGINE" ]; then
if command -v docker > /dev/null 2>&1; then
CONTAINER_ENGINE=docker
elif command -v podman > /dev/null 2>&1; then
CONTAINER_ENGINE=podman
else
echo "Neither docker nor podman was found on PATH." >&2
exit 1
fi
fi
echo "Using container engine: $CONTAINER_ENGINE"
ARCHS=(x86_64 aarch64) ARCHS=(x86_64 aarch64)
BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine) BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine)
COMPRESSION_VARIANTS=(ubuntu-focal alpine) COMPRESSION_VARIANTS=(ubuntu-focal alpine)
# Build one image per (variant, arch). Passing several --platform flags to a single tagged build
# does not produce a multi-arch tag - only the last architecture keeps the tag, and the run below
# then sees a platform mismatch, treats the image as missing and tries to pull it from a registry.
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
docker_platforms=""
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
docker_platforms="$docker_platforms --platform linux/${arch}" echo "Building base build image for $variant on $arch..."
$CONTAINER_ENGINE build -t velocity-native-build:$variant-$arch --platform linux/${arch} \
-f build-support/$variant.Dockerfile .
done done
echo "Building base build image for $variant..."
docker build -t velocity-native-build:$variant $docker_platforms -f build-support/$variant.Dockerfile .
done done
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
echo "Building native crypto for $arch on $variant..." echo "Building native crypto for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh"
done done
for variant in "${COMPRESSION_VARIANTS[@]}"; do for variant in "${COMPRESSION_VARIANTS[@]}"; do
echo "Building native compression for $arch on $variant..." echo "Building native compression for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh"
done done
done done
+1 -2
View File
@@ -34,8 +34,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e
jlong sourceAddress, jlong sourceAddress,
jint sourceLength, jint sourceLength,
jlong destinationAddress, jlong destinationAddress,
jint destinationLength, jint destinationLength)
jlong maximumSize)
{ {
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx; struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress, enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
@@ -56,24 +56,45 @@ public class JavaVelocityCompressor implements VelocityCompressor {
final int origIdx = source.readerIndex(); final int origIdx = source.readerIndex();
inflater.setInput(source.nioBuffer()); inflater.setInput(source.nioBuffer());
int totalProduced = 0;
try { try {
final int readable = source.readableBytes(); final int readable = source.readableBytes();
while (!inflater.finished() && inflater.getBytesRead() < readable) { while (!inflater.finished() && inflater.getBytesRead() < readable) {
if (totalProduced >= uncompressedSize) {
throw new DataFormatException("Decompressed data exceeds the claimed uncompressed size "
+ "of " + uncompressedSize + " bytes");
}
final int remaining = uncompressedSize - totalProduced;
if (!destination.isWritable()) { if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE); destination.ensureWritable(Math.min(ZLIB_BUFFER_SIZE, remaining));
} }
ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(),
destination.writableBytes()); destination.writableBytes());
// Never let a single inflate step write past the claimed size
if (destNioBuf.remaining() > remaining) {
destNioBuf.limit(destNioBuf.position() + remaining);
}
int produced = inflater.inflate(destNioBuf); int produced = inflater.inflate(destNioBuf);
if (produced == 0 && !inflater.finished()) {
// Output space was available yet the inflater made no progress: the stream is truncated
// or corrupt (this also covers a peer that over-reported the uncompressed size).
throw new DataFormatException("Received a truncated or malformed deflate stream, "
+ "expected " + uncompressedSize + " bytes");
}
totalProduced += produced;
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
if (!inflater.finished()) { if (!inflater.finished()) {
throw new DataFormatException("Received a deflate stream that was too large, wanted " throw new DataFormatException("Received a truncated or malformed deflate stream, expected "
+ uncompressedSize); + uncompressedSize + " bytes");
} }
source.readerIndex(origIdx + inflater.getTotalIn());
source.readerIndex(origIdx + (int) inflater.getBytesRead());
} finally { } finally {
inflater.reset(); inflater.reset();
} }
@@ -102,7 +123,7 @@ public class JavaVelocityCompressor implements VelocityCompressor {
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
source.readerIndex(origIdx + deflater.getTotalIn()); source.readerIndex(origIdx + (int) deflater.getBytesRead());
deflater.reset(); deflater.reset();
} }
@@ -54,8 +54,8 @@ public class MoreByteBufUtils {
BufferPreference preferred = nativeStuff.preferredBufferType(); BufferPreference preferred = nativeStuff.preferredBufferType();
return switch (preferred) { return switch (preferred) {
case DIRECT_PREFERRED, HEAP_PREFERRED -> case DIRECT_PREFERRED, HEAP_PREFERRED ->
// The native prefers this type, but doesn't strictly require we provide it. // The native prefers this type, but doesn't strictly require we provide it.
true; true;
case DIRECT_REQUIRED -> buf.hasMemoryAddress(); case DIRECT_REQUIRED -> buf.hasMemoryAddress();
case HEAP_REQUIRED -> buf.hasArray(); case HEAP_REQUIRED -> buf.hasArray();
}; };
@@ -17,6 +17,7 @@
package com.velocitypowered.natives.compression; package com.velocitypowered.natives.compression;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@@ -77,6 +78,83 @@ class VelocityCompressorTest {
check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32));
} }
private static final int BOMB_ACTUAL_SIZE = 1 << 20;
private static final int BOMB_LYING_CLAIM = 1024;
@Test
void javaRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = JavaVelocityCompressor.FACTORY
.create(Deflater.DEFAULT_COMPRESSION);
DataFormatException ex = assertRejectsDecompressionBomb(compressor);
// The Java compressor's size guard names the claimed size in its message, so operators can
// tell an over-size rejection apart from a genuinely corrupt stream.
assertTrue(ex.getMessage().contains(String.valueOf(BOMB_LYING_CLAIM)),
"rejection must originate from the uncompressed-size guard, got: " + ex.getMessage());
}
@Test
@EnabledOnOs({LINUX})
void nativeRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION);
if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) {
compressor.close();
fail("Loaded regular compressor");
}
// libdeflate rejects with its own native-origin message ("uncompressed size is inaccurate"),
// so we only assert the behavioural guarantee here, not the message text.
assertRejectsDecompressionBomb(compressor);
}
/**
* Asserts that a compressor refuses a decompression bomb: a small, valid deflate stream whose
* claimed uncompressed size is far smaller than what it actually inflates to. Verifies the same
* stream round-trips when the claimed size is honest (proving the rejection is caused by the
* under-reported size, not corrupt input) and that no output is written past the claimed size.
* Closes the compressor before returning the exception thrown by the rejected inflate.
*/
private DataFormatException assertRejectsDecompressionBomb(VelocityCompressor compressor)
throws DataFormatException {
// Direct buffers so this works for the native compressor, which requires them.
ByteBuf source = Unpooled.directBuffer(BOMB_ACTUAL_SIZE);
ByteBuf compressed = Unpooled.directBuffer();
try {
source.writeZero(BOMB_ACTUAL_SIZE);
compressor.deflate(source, compressed);
final int compressedSize = compressed.readableBytes();
assertTrue(compressedSize < BOMB_ACTUAL_SIZE / 100,
"sanity: payload really is a decompression bomb (" + compressedSize + " -> "
+ BOMB_ACTUAL_SIZE + ")");
// Positive control: the compressed stream is perfectly valid and round-trips when the peer
// tells the truth about its uncompressed size.
ByteBuf honest = Unpooled.directBuffer();
try {
compressor.inflate(compressed.duplicate(), honest, BOMB_ACTUAL_SIZE);
assertEquals(BOMB_ACTUAL_SIZE, honest.readableBytes(),
"valid stream must fully decompress when the claimed size is honest");
} finally {
honest.release();
}
// Attack: same valid stream, but a tiny claimed size. inflate must refuse rather than grow
// the destination without bound.
ByteBuf decompressed = Unpooled.directBuffer();
try {
DataFormatException ex = assertThrows(DataFormatException.class,
() -> compressor.inflate(compressed.duplicate(), decompressed, BOMB_LYING_CLAIM));
assertTrue(decompressed.writerIndex() <= BOMB_LYING_CLAIM,
"inflate must not write past the claimed uncompressed size");
return ex;
} finally {
decompressed.release();
}
} finally {
source.release();
compressed.release();
compressor.close();
}
}
private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier) private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier)
throws DataFormatException { throws DataFormatException {
ByteBuf source = bufSupplier.get(); ByteBuf source = bufSupplier.get();
+39 -62
View File
@@ -1,14 +1,16 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import io.papermc.fill.model.BuildChannel
plugins { plugins {
application application
id("velocity-init-manifest") id("velocity-init-manifest")
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
alias(libs.plugins.fill)
} }
application { application {
mainClass.set("com.velocitypowered.proxy.Velocity") mainClass.set("com.velocitypowered.proxy.Velocity")
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true"); applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true")
} }
tasks { tasks {
@@ -25,67 +27,12 @@ tasks {
} }
shadowJar { shadowJar {
filesMatching("META-INF/org/apache/logging/log4j/core/config/plugins/**") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
transform(Log4j2PluginsCacheFileTransformer::class.java) transform(Log4j2PluginsCacheFileTransformer::class.java)
// Exclude all the collection types we don"t intend to use
exclude("it/unimi/dsi/fastutil/booleans/**")
exclude("it/unimi/dsi/fastutil/bytes/**")
exclude("it/unimi/dsi/fastutil/chars/**")
exclude("it/unimi/dsi/fastutil/doubles/**")
exclude("it/unimi/dsi/fastutil/floats/**")
exclude("it/unimi/dsi/fastutil/longs/**")
exclude("it/unimi/dsi/fastutil/shorts/**")
// Exclude the fastutil IO utilities - we don"t use them.
exclude("it/unimi/dsi/fastutil/io/**")
// Exclude most of the int types - Object2IntMap have a values() method that returns an
// IntCollection, and we need Int2ObjectMap
exclude("it/unimi/dsi/fastutil/ints/*Int2Boolean*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Byte*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Char*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Double*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Float*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Int*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Long*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
exclude("it/unimi/dsi/fastutil/ints/IntList*")
exclude("it/unimi/dsi/fastutil/ints/IntHeap*")
exclude("it/unimi/dsi/fastutil/ints/IntOpen*")
exclude("it/unimi/dsi/fastutil/ints/IntRB*")
exclude("it/unimi/dsi/fastutil/ints/IntSorted*")
exclude("it/unimi/dsi/fastutil/ints/*Priority*")
exclude("it/unimi/dsi/fastutil/ints/*BigList*")
// Try to exclude everything BUT Object2Int{LinkedOpen,Open,CustomOpen}HashMap
exclude("it/unimi/dsi/fastutil/objects/*ObjectArray*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object*Big*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Boolean*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Byte*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Char*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Double*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Float*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntArray*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntRB*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Long*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Object*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Reference*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Short*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectRB*")
exclude("it/unimi/dsi/fastutil/objects/*Reference*")
// Exclude Checker Framework annotations // Exclude Checker Framework annotations
exclude("org/checkerframework/checker/**") exclude("org/checkerframework/checker/**")
@@ -106,12 +53,42 @@ tasks {
workingDir = file("run").also(File::mkdirs) workingDir = file("run").also(File::mkdirs)
standardInput = System.`in` // Doesn't work? standardInput = System.`in` // Doesn't work?
} }
withType<JavaCompile>().configureEach {
options.compilerArgs.addAll(
listOf(
"-Alog4j.graalvm.groupId=${project.group}",
"-Alog4j.graalvm.artifactId=${project.name}"
)
)
}
}
val projectVersion = version as String
fill {
project("velocity")
build {
channel = BuildChannel.STABLE
versionFamily("4.0.0")
version(projectVersion)
if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) {
throw IllegalArgumentException("Version family does not match project version")
}
downloads {
register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile }
nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" }
}
}
}
} }
dependencies { dependencies {
implementation(project(":velocity-api")) implementation(project(":velocity-api"))
implementation(project(":velocity-native")) implementation(project(":velocity-native"))
implementation(project(":velocity-proxy-log4j2-plugin"))
implementation(libs.bundles.log4j) implementation(libs.bundles.log4j)
implementation(libs.kyori.ansi) implementation(libs.kyori.ansi)
@@ -136,7 +113,6 @@ dependencies {
implementation(libs.fastutil) implementation(libs.fastutil)
implementation(platform(libs.adventure.bom)) implementation(platform(libs.adventure.bom))
implementation(libs.adventure.text.serializer.json.legacy.impl) implementation(libs.adventure.text.serializer.json.legacy.impl)
implementation(libs.adventure.facet)
implementation(libs.completablefutures) implementation(libs.completablefutures)
implementation(libs.nightconfig) implementation(libs.nightconfig)
implementation(libs.bstats) implementation(libs.bstats)
@@ -148,4 +124,5 @@ dependencies {
testImplementation(libs.mockito) testImplementation(libs.mockito)
annotationProcessor(libs.auto.service) annotationProcessor(libs.auto.service)
annotationProcessor(libs.log4j.core)
} }
-4
View File
@@ -1,4 +0,0 @@
dependencies {
implementation(libs.bundles.log4j)
annotationProcessor(libs.log4j.core)
}
@@ -21,10 +21,8 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.stream.Collectors;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bstats.MetricsBase; import org.bstats.MetricsBase;
@@ -120,38 +118,28 @@ public class Metrics {
() -> server.getVersion().getVersion())); () -> server.getVersion().getVersion()));
metrics.addCustomChart(new DrilldownPie("java_version", () -> { metrics.addCustomChart(new DrilldownPie("java_version", () -> {
Map<String, Map<String, Integer>> map = new HashMap<>(); Runtime.Version version = Runtime.version();
String javaVersion = System.getProperty("java.version");
Map<String, Integer> entry = new HashMap<>();
entry.put(javaVersion, 1);
// http://openjdk.java.net/jeps/223 return Map.of(
// Java decided to change their versioning scheme and in doing so modified the "Java " + version.feature(),
// java.version system property to return $major[.$minor][.$security][-ea], as opposed to Map.of(javaVersion(version), 1));
// 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1",
// otherwise, 9+
String majorVersion = javaVersion.split("\\.")[0];
String release;
int indexOf = javaVersion.lastIndexOf('.');
if (majorVersion.equals("1")) {
release = "Java " + javaVersion.substring(0, indexOf);
} else {
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
// would it valid strings for the major may potentially include values such as -ea to
// denote a pre release
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
if (versionMatcher.find()) {
majorVersion = versionMatcher.group(0);
}
release = "Java " + majorVersion;
}
map.put(release, entry);
return map;
})); }));
} }
} }
} /**
* Recreates the exact {@code java.version} system property value from a {@link Runtime.Version}.
*
* <p>Per <a href="https://openjdk.org/jeps/223">JEP 223</a>, {@code java.version} is
* {@code $VNUM(-$PRE)?}; the build and optional segments only appear in {@code java.runtime.version}.
*
* @param v the runtime version
* @return the value {@code java.version} would hold on this JVM
*/
private static String javaVersion(Runtime.Version v) {
return v.version().stream()
.map(Object::toString)
.collect(Collectors.joining("."))
+ v.pre().map(p -> "-" + p).orElse("");
}
}
@@ -47,11 +47,6 @@ public class Velocity {
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir")); 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 // Disable the resource leak detector by default as it reduces performance. Allow the user to
// override this if desired. // override this if desired.
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) { if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {
@@ -24,6 +24,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; 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.ProxyReloadEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@@ -81,7 +82,6 @@ import java.net.http.HttpClient;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.KeyPair; import java.security.KeyPair;
import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -104,8 +104,8 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; 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.GlobalTranslator;
import net.kyori.adventure.translation.TranslationStore;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bstats.MetricsBase; import org.bstats.MetricsBase;
@@ -119,7 +119,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/ */
public class VelocityServer implements ProxyServer, ForwardingAudience { 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); private static final Logger logger = LogManager.getLogger(VelocityServer.class);
public static final Gson GENERAL_GSON = new GsonBuilder() public static final Gson GENERAL_GSON = new GsonBuilder()
@@ -150,6 +150,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
) )
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE) .registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
.create(); .create();
private static final int PRE_SHUTDOWN_TIMEOUT =
Integer.getInteger("velocity.pre-shutdown-timeout", 10);
private final ConnectionManager cm; private final ConnectionManager cm;
private final ProxyOptions options; private final ProxyOptions options;
@@ -163,6 +165,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>(); private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>(); private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final Object sessionIdLock = new Object();
private volatile @Nullable UUID sessionId;
private final VelocityConsole console; private final VelocityConsole console;
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter; private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter; private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
@@ -216,7 +220,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
ProxyVersion version = getVersion(); ProxyVersion version = getVersion();
PluginDescription description = new VelocityPluginDescription( PluginDescription description = new VelocityPluginDescription(
"velocity", version.getName(), version.getVersion(), "The Velocity proxy", "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(),
Collections.emptyList(), null);
VelocityPluginContainer container = new VelocityPluginContainer(description); VelocityPluginContainer container = new VelocityPluginContainer(description);
container.setInstance(VelocityVirtualPlugin.INSTANCE); container.setInstance(VelocityVirtualPlugin.INSTANCE);
return container; return container;
@@ -238,8 +244,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
console.setupStreams(); console.setupStreams();
pluginManager.registerPlugin(this.createVirtualPlugin()); pluginManager.registerPlugin(this.createVirtualPlugin());
registerTranslations();
// Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds // 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... // dangerous, right? We're well within the realm of factoring such a key...
// //
@@ -288,6 +292,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
this.doStartupConfigLoad(); this.doStartupConfigLoad();
registerTranslations();
for (ServerInfo cliServer : options.getServers()) { for (ServerInfo cliServer : options.getServers()) {
servers.register(cliServer); servers.register(cliServer);
} }
@@ -338,8 +344,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
} }
private void registerTranslations() { private void registerTranslations() {
final TranslationStore.StringBased<MessageFormat> translationRegistry = final MiniMessageTranslationStore translationRegistry =
TranslationStore.messageFormat(Key.key("velocity", "translations")); MiniMessageTranslationStore.create(Key.key("velocity", "translations"));
translationRegistry.defaultLocale(Locale.US); translationRegistry.defaultLocale(Locale.US);
try { try {
ResourceUtils.visitResources(VelocityServer.class, path -> { ResourceUtils.visitResources(VelocityServer.class, path -> {
@@ -482,23 +488,46 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return false; return false;
} }
// Re-register servers. If a server is being replaced, make sure to note what players need to // Re-register servers. If a server is being replaced or removed, make sure to note what
// move back to a fallback server. // players need to move back to a fallback server.
Collection<ConnectedPlayer> evacuate = new ArrayList<>(); Collection<ConnectedPlayer> evacuate = new ArrayList<>();
for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
if (newConfiguration.getServers().keySet().stream()
.anyMatch(name -> name.equalsIgnoreCase(entry.getKey()))) {
continue;
}
RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
if (rs == null) {
continue;
}
for (Player player : rs.getPlayersConnected()) {
if (!(player instanceof ConnectedPlayer)) {
throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ " on server " + rs.getServerInfo().getName() + ", got "
+ player.getClass().getName());
}
evacuate.add((ConnectedPlayer) player);
}
servers.unregister(rs.getServerInfo());
}
for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) { for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) {
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())); ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
Optional<RegisteredServer> rs = servers.getServer(entry.getKey()); RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
if (rs.isEmpty()) { if (rs == null) {
servers.register(newInfo); servers.register(newInfo);
} else if (!rs.get().getServerInfo().equals(newInfo)) { } else if (!rs.getServerInfo().equals(newInfo)) {
for (Player player : rs.get().getPlayersConnected()) { for (Player player : rs.getPlayersConnected()) {
if (!(player instanceof ConnectedPlayer)) { if (!(player instanceof ConnectedPlayer)) {
throw new IllegalStateException("ConnectedPlayer not found for player " + player throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ " in server " + rs.get().getServerInfo().getName()); + " on server " + rs.getServerInfo().getName() + ", got "
+ player.getClass().getName());
} }
evacuate.add((ConnectedPlayer) player); evacuate.add((ConnectedPlayer) player);
} }
servers.unregister(rs.get().getServerInfo()); servers.unregister(rs.getServerInfo());
servers.register(newInfo); servers.register(newInfo);
} }
} }
@@ -578,6 +607,20 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
// done first to refuse new connections // done first to refuse new connections
cm.shutdown(); 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()); ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
for (ConnectedPlayer player : players) { for (ConnectedPlayer player : players) {
player.disconnect(reason); player.disconnect(reason);
@@ -633,7 +676,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
} }
/** /**
* Calls {@link #shutdown(boolean, Component)} with the default reason "Proxy shutting down." * Calls {@link #shutdown(boolean, Component)} with the default reason "Proxy shutting down".
* *
* @param explicitExit whether the user explicitly shut down the proxy * @param explicitExit whether the user explicitly shut down the proxy
*/ */
@@ -726,6 +769,36 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection); connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection); connectionsByUuid.remove(connection.getUniqueId(), connection);
connection.disconnected(); connection.disconnected();
if (this.sessionId != null && connectionsByUuid.isEmpty()) {
synchronized (this.sessionIdLock) {
if (connectionsByUuid.isEmpty()) {
this.sessionId = null;
}
}
}
}
/**
* Returns the metrics session ID for this proxy, generating one if none is currently active. The
* ID is shared by every player connected during a populated period and is regenerated once the
* proxy empties.
*
* @return the current session ID
*/
public UUID getSessionId() {
UUID uuid = this.sessionId;
if (uuid != null) {
return uuid;
}
synchronized (this.sessionIdLock) {
uuid = this.sessionId;
if (uuid == null) {
uuid = UUID.randomUUID();
this.sessionId = uuid;
}
return uuid;
}
} }
@Override @Override
@@ -817,7 +890,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
public VelocityChannelRegistrar getChannelRegistrar() { public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar; return channelRegistrar;
} }
@Override @Override
public boolean isShuttingDown() { public boolean isShuttingDown() {
return shutdownInProgress.get(); return shutdownInProgress.get();
@@ -53,15 +53,23 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
viewer.getProtocolVersion(), viewer.getProtocolVersion(),
viewer.translateMessage(this.bar.name()) 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 true;
} }
return false; 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) { public boolean viewerRemove(final ConnectedPlayer viewer) {
if (this.viewers.remove(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 true;
} }
return false; return false;
@@ -84,7 +92,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
this.bar, this.bar,
new ComponentHolder(viewer.getProtocolVersion(), translated) 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); final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { 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); final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { 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); final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { 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); final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { 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 0;
}); });
} }
return potentials.get(0); return potentials.getFirst();
} }
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap()); 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.CommandMeta;
import com.velocitypowered.api.command.CommandResult; import com.velocitypowered.api.command.CommandResult;
import com.velocitypowered.api.command.CommandSource; 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.CommandExecuteEvent;
import com.velocitypowered.api.event.command.PostCommandInvocationEvent; import com.velocitypowered.api.event.command.PostCommandInvocationEvent;
import com.velocitypowered.api.plugin.PluginManager; 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.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedBy;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@@ -140,7 +140,7 @@ public class VelocityCommandManager implements CommandManager {
command + " implements multiple registrable Command subinterfaces: " command + " implements multiple registrable Command subinterfaces: "
+ implementedInterfaces); + implementedInterfaces);
} else { } 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()); CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
if (isSyntaxError) { if (isSyntaxError) {
final Message message = e.getRawMessage(); final Message message = e.getRawMessage();
if (message instanceof VelocityBrigadierMessage velocityMessage) { if (message instanceof ComponentLike componentLike) {
source.sendMessage(velocityMessage.asComponent().applyFallbackStyle(NamedTextColor.RED)); source.sendMessage(componentLike.asComponent().applyFallbackStyle(NamedTextColor.RED));
} else { } else {
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED)); source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
} }
@@ -256,7 +256,7 @@ public class VelocityCommandManager implements CommandManager {
} }
} catch (final Throwable e) { } catch (final Throwable e) {
// Ugly, ugly swallowing of everything Throwable, because plugins are naughty. // 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 { } finally {
eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result)); eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result));
} }
@@ -400,4 +400,4 @@ public class VelocityCommandManager implements CommandManager {
return MoreExecutors.directExecutor(); return MoreExecutors.directExecutor();
} }
} }
} }
@@ -70,33 +70,34 @@ public final class VelocityCommands {
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant); maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
} }
if (delegate instanceof LiteralCommandNode<CommandSource> lcn) { return switch (delegate) {
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true); case LiteralCommandNode<CommandSource> lcn -> {
literalBuilder.executes(maybeCommand); var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
// we also need to wrap any children literalBuilder.executes(maybeCommand);
for (final CommandNode<CommandSource> child : delegate.getChildren()) { // we also need to wrap any children
literalBuilder.then(wrap(child, registrant)); 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) { case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand)
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant)); .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(); default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
} 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());
}
} }
// Normalization // Normalization
@@ -133,7 +134,7 @@ public final class VelocityCommands {
if (nodes.isEmpty()) { if (nodes.isEmpty()) {
throw new IllegalArgumentException("Cannot read alias from empty node list"); 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"; public static final String ARGS_NODE_NAME = "arguments";
@@ -118,14 +118,12 @@ public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, St
if (this == o) { if (this == o) {
return true; return true;
} }
if (!(o instanceof VelocityArgumentCommandNode)) { if (!(o instanceof VelocityArgumentCommandNode that)) {
return false; return false;
} }
if (!super.equals(o)) { if (!super.equals(that)) {
return false; return false;
} }
final VelocityArgumentCommandNode<?, ?> that = (VelocityArgumentCommandNode<?, ?>) o;
return this.type.equals(that.type); 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.TextComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/** /**
* Implements the Velocity default {@code /glist} command. * Implements the Velocity default {@code /glist} command.
@@ -111,7 +112,7 @@ public class GlistCommand {
if (registeredServer.isEmpty()) { if (registeredServer.isEmpty()) {
source.sendMessage( source.sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST CommandMessages.SERVER_DOES_NOT_EXIST
.arguments(Component.text(serverName))); .arguments(Argument.string("server", serverName)));
return -1; return -1;
} }
sendServerPlayers(source, registeredServer.get(), false); sendServerPlayers(source, registeredServer.get(), false);
@@ -126,7 +127,8 @@ public class GlistCommand {
? "velocity.command.glist-player-singular" ? "velocity.command.glist-player-singular"
: "velocity.command.glist-player-plural" : "velocity.command.glist-player-plural"
).color(NamedTextColor.YELLOW) ).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()); target.sendMessage(msg.build());
} }
@@ -35,6 +35,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/** /**
* Implements the Velocity default {@code /send} command. * Implements the Velocity default {@code /send} command.
@@ -121,7 +122,7 @@ public class SendCommand {
if (maybeServer.isEmpty()) { if (maybeServer.isEmpty()) {
context.getSource().sendMessage( context.getSource().sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName)) CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Argument.string("server", serverName))
); );
return 0; return 0;
} }
@@ -133,7 +134,7 @@ public class SendCommand {
&& !Objects.equals(player, "all") && !Objects.equals(player, "all")
&& !Objects.equals(player, "current")) { && !Objects.equals(player, "current")) {
context.getSource().sendMessage( context.getSource().sendMessage(
CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player)) CommandMessages.PLAYER_NOT_FOUND.arguments(Argument.string("player", player))
); );
return 0; return 0;
} }
@@ -37,6 +37,7 @@ import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/** /**
* Implements Velocity's {@code /server} command. * Implements Velocity's {@code /server} command.
@@ -76,7 +77,7 @@ public final class ServerCommand {
final Optional<RegisteredServer> toConnect = server.getServer(serverName); final Optional<RegisteredServer> toConnect = server.getServer(serverName);
if (toConnect.isEmpty()) { if (toConnect.isEmpty()) {
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST
.arguments(Component.text(serverName))); .arguments(Argument.string("server", serverName)));
return -1; return -1;
} }
@@ -135,7 +136,7 @@ public final class ServerCommand {
} else { } else {
playersTextComponent.key("velocity.command.server-tooltip-players-online"); 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)) { if (serverInfo.getName().equals(currentPlayerServer)) {
serverTextComponent.color(NamedTextColor.GREEN) serverTextComponent.color(NamedTextColor.GREEN)
.hoverEvent( .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.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -160,14 +161,17 @@ public final class VelocityCommand {
.decoration(TextDecoration.BOLD, true) .decoration(TextDecoration.BOLD, true)
.color(VELOCITY_COLOR) .color(VELOCITY_COLOR)
.append(Component.text() .append(Component.text()
.content(version.getVersion()) .content(version.getVersion())
.decoration(TextDecoration.BOLD, false)) .decoration(TextDecoration.BOLD, false))
.hoverEvent(Component.translatable("velocity.command.version-offer-copy-version"))
.clickEvent(ClickEvent.copyToClipboard(version.getName() + " "
+ version.getVersion()))
.build(); .build();
final Component copyright = Component final Component copyright = Component
.translatable("velocity.command.version-copyright", .translatable("velocity.command.version-copyright",
Component.text(version.getVendor()), Argument.string("vendor", version.getVendor()),
Component.text(version.getName()), Argument.string("name", version.getName()),
Component.text(LocalDate.now().getYear())); Argument.component("year", Component.text(LocalDate.now().getYear())));
source.sendMessage(velocity); source.sendMessage(velocity);
source.sendMessage(copyright); source.sendMessage(copyright);
@@ -176,8 +180,7 @@ public final class VelocityCommand {
.append(Component.text() .append(Component.text()
.content("PaperMC") .content("PaperMC")
.color(NamedTextColor.GREEN) .color(NamedTextColor.GREEN)
.clickEvent( .clickEvent(ClickEvent.openUrl(VelocityServer.VELOCITY_URL))
ClickEvent.openUrl("https://papermc.io/software/velocity"))
.build()) .build())
.append(Component.text(" - ")) .append(Component.text(" - "))
.append(Component.text() .append(Component.text()
@@ -221,7 +224,7 @@ public final class VelocityCommand {
final TranslatableComponent output = Component.translatable() final TranslatableComponent output = Component.translatable()
.key("velocity.command.plugins-list") .key("velocity.command.plugins-list")
.color(NamedTextColor.YELLOW) .color(NamedTextColor.YELLOW)
.arguments(listBuilder.build()) .arguments(Argument.component("plugins", listBuilder.build()))
.build(); .build();
source.sendMessage(output); source.sendMessage(output);
return Command.SINGLE_SUCCESS; return Command.SINGLE_SUCCESS;
@@ -237,17 +240,17 @@ public final class VelocityCommand {
hoverText.append(Component.newline()); hoverText.append(Component.newline());
hoverText.append(Component.translatable( hoverText.append(Component.translatable(
"velocity.command.plugin-tooltip-website", "velocity.command.plugin-tooltip-website",
Component.text(url))); Argument.component("url", Component.text(url))));
}); });
if (!description.getAuthors().isEmpty()) { if (!description.getAuthors().isEmpty()) {
hoverText.append(Component.newline()); hoverText.append(Component.newline());
if (description.getAuthors().size() == 1) { if (description.getAuthors().size() == 1) {
hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author", hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author",
Component.text(description.getAuthors().get(0)))); Component.text(description.getAuthors().getFirst())));
} else { } else {
hoverText.append( hoverText.append(
Component.translatable("velocity.command.plugin-tooltip-author", 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)) .requiresWithContext((context, reader) -> requirement.test(context))
.executes(callback) .executes(callback)
.suggests((context, builder) -> { .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); final I invocation = invocationFactory.create(context);
return command.suggestAsync(invocation).thenApply(suggestions -> { return command.suggestAsync(invocation).thenApply(suggestions -> {
for (String value : suggestions) { for (String value : suggestions) {
Preconditions.checkNotNull(value, "suggestion"); Preconditions.checkNotNull(value, "suggestion");
builder.suggest(value); offsetBuilder.suggest(value);
} }
return builder.build(); return offsetBuilder.build();
}); });
}) })
.build(); .build();
@@ -16,13 +16,49 @@
*/ */
package com.velocitypowered.proxy.config; package com.velocitypowered.proxy.config;
import com.electronwill.nightconfig.core.Config;
/** /**
* Supported passthrough modes for ping passthrough. * Object to contain all the things that can be toggled for ping passthrough.
*
* @param version Whether the version should be passed through.
* @param players Whether the player count should be passed through.
* @param description Whether the description should be passed through.
* @param favicon Whether the favicon should be passed through.
* @param modinfo Whether the modinfo should be passed through.
*/ */
public enum PingPassthroughMode { public record PingPassthroughMode(boolean version, boolean players,
DISABLED, boolean description, boolean favicon, boolean modinfo) {
MODS, public static final PingPassthroughMode DEFAULT = new PingPassthroughMode();
DESCRIPTION,
ALL /**
* Creates a default PingPassthroughMode.
*/
private PingPassthroughMode() {
this(false, false, false, false, false);
}
/**
* Returns a PingPassthroughMode from a config section, or the default if the section is null.
* Based on the code for PacketLimiterConfig.
*
* @param config The configuration object to parse.
* @return The PingPassthroughMode, or the default if {@code config} is null.
*/
public static PingPassthroughMode fromConfig(Config config) {
if (config == null) {
return DEFAULT;
}
return new PingPassthroughMode(
config.getOrElse("version", DEFAULT.version()),
config.getOrElse("players", DEFAULT.players()),
config.getOrElse("description", DEFAULT.description()),
config.getOrElse("favicon", DEFAULT.favicon()),
config.getOrElse("modinfo", DEFAULT.modinfo()));
}
public boolean enabled() {
return this.version || this.players || this.description || this.favicon
|| this.modinfo;
}
} }
@@ -29,7 +29,10 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.config.migration.ConfigurationMigration; import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
import com.velocitypowered.proxy.config.migration.ForwardingMigration; import com.velocitypowered.proxy.config.migration.ForwardingMigration;
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration; 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.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
import com.velocitypowered.proxy.config.migration.PingPassthroughMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration; import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -77,7 +80,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose @Expose
private boolean onlineModeKickExistingPlayers = false; private boolean onlineModeKickExistingPlayers = false;
@Expose @Expose
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; private PingPassthroughMode pingPassthrough = PingPassthroughMode.DEFAULT;
@Expose @Expose
private boolean samplePlayersInPing = false; private boolean samplePlayersInPing = false;
private final Servers servers; private final Servers servers;
@@ -93,6 +96,8 @@ public class VelocityConfiguration implements ProxyConfig {
private @Nullable Favicon favicon; private @Nullable Favicon favicon;
@Expose @Expose
private boolean forceKeyAuthentication = true; // Added in 1.19 private boolean forceKeyAuthentication = true; // Added in 1.19
@Expose
private PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.DEFAULT;
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced, private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
Query query, Metrics metrics) { Query query, Metrics metrics) {
@@ -109,7 +114,7 @@ public class VelocityConfiguration implements ProxyConfig {
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers, boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
boolean forceKeyAuthentication) { boolean forceKeyAuthentication, PacketLimiterConfig packetLimiterConfig) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
@@ -128,6 +133,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.query = query; this.query = query;
this.metrics = metrics; this.metrics = metrics;
this.forceKeyAuthentication = forceKeyAuthentication; this.forceKeyAuthentication = forceKeyAuthentication;
this.packetLimiterConfig = packetLimiterConfig;
} }
/** /**
@@ -156,19 +162,16 @@ public class VelocityConfiguration implements ProxyConfig {
} }
switch (playerInfoForwardingMode) { switch (playerInfoForwardingMode) {
case NONE: case NONE -> logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
+ "from the proxy and will have offline-mode UUIDs."); + "from the proxy and will have offline-mode UUIDs.");
break; case MODERN, BUNGEEGUARD -> {
case MODERN:
case BUNGEEGUARD:
if (forwardingSecret == null || forwardingSecret.length == 0) { if (forwardingSecret == null || forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set. This is required for security."); logger.error("You don't have a forwarding secret set. This is required for security.");
valid = false; valid = false;
} }
break; }
default: default -> {
break; }
} }
if (servers.getServers().isEmpty()) { if (servers.getServers().isEmpty()) {
@@ -449,6 +452,10 @@ public class VelocityConfiguration implements ProxyConfig {
return advanced.isEnableReusePort(); return advanced.isEnableReusePort();
} }
public PacketLimiterConfig getPacketLimiterConfig() {
return packetLimiterConfig;
}
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
@@ -466,6 +473,7 @@ public class VelocityConfiguration implements ProxyConfig {
.add("favicon", favicon) .add("favicon", favicon)
.add("enablePlayerAddressLogging", enablePlayerAddressLogging) .add("enablePlayerAddressLogging", enablePlayerAddressLogging)
.add("forceKeyAuthentication", forceKeyAuthentication) .add("forceKeyAuthentication", forceKeyAuthentication)
.add("packetLimiterConfig", packetLimiterConfig)
.toString(); .toString();
} }
@@ -504,7 +512,10 @@ public class VelocityConfiguration implements ProxyConfig {
new ForwardingMigration(), new ForwardingMigration(),
new KeyAuthenticationMigration(), new KeyAuthenticationMigration(),
new MotdMigration(), new MotdMigration(),
new TransferIntegrationMigration() new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration(),
new PacketLimiterMigration(),
new PingPassthroughMigration(),
}; };
for (final ConfigurationMigration migration : migrations) { for (final ConfigurationMigration migration : migrations) {
@@ -515,7 +526,7 @@ public class VelocityConfiguration implements ProxyConfig {
String forwardingSecretString = System.getenv().getOrDefault( String forwardingSecretString = System.getenv().getOrDefault(
"VELOCITY_FORWARDING_SECRET", ""); "VELOCITY_FORWARDING_SECRET", "");
if (forwardingSecretString.isEmpty()) { if (forwardingSecretString.isBlank()) {
final String forwardSecretFile = config.get("forwarding-secret-file"); final String forwardSecretFile = config.get("forwarding-secret-file");
final Path secretPath = forwardSecretFile == null final Path secretPath = forwardSecretFile == null
? defaultForwardingSecretPath ? defaultForwardingSecretPath
@@ -528,7 +539,11 @@ public class VelocityConfiguration implements ProxyConfig {
"The file " + forwardSecretFile + " is not a valid file or it is a directory."); "The file " + forwardSecretFile + " is not a valid file or it is a directory.");
} }
} else { } 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); final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
@@ -542,9 +557,7 @@ public class VelocityConfiguration implements ProxyConfig {
final CommentedConfig metricsConfig = config.get("metrics"); final CommentedConfig metricsConfig = config.get("metrics");
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse( final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
"player-info-forwarding-mode", PlayerInfoForwarding.NONE); "player-info-forwarding-mode", PlayerInfoForwarding.NONE);
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", final PingPassthroughMode pingPassthrough = PingPassthroughMode.fromConfig(config.get("ping-passthrough"));
PingPassthroughMode.DISABLED);
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false); final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565"); final String bind = config.getOrElse("bind", "0.0.0.0:25565");
@@ -557,6 +570,7 @@ public class VelocityConfiguration implements ProxyConfig {
final boolean kickExisting = config.getOrElse("kick-existing-players", false); final boolean kickExisting = config.getOrElse("kick-existing-players", false);
final boolean enablePlayerAddressLogging = config.getOrElse( final boolean enablePlayerAddressLogging = config.getOrElse(
"enable-player-address-logging", true); "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 // Throw an exception if the forwarding-secret file is empty and the proxy is using a
// forwarding mode that requires it. // forwarding mode that requires it.
@@ -576,7 +590,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingMode, forwardingMode,
forwardingSecret, forwardingSecret,
kickExisting, kickExisting,
pingPassthroughMode, pingPassthrough,
samplePlayersInPing, samplePlayersInPing,
enablePlayerAddressLogging, enablePlayerAddressLogging,
new Servers(serversConfig), new Servers(serversConfig),
@@ -584,7 +598,8 @@ public class VelocityConfiguration implements ProxyConfig {
new Advanced(advancedConfig), new Advanced(advancedConfig),
new Query(queryConfig), new Query(queryConfig),
new Metrics(metricsConfig), new Metrics(metricsConfig),
forceKeyAuthentication forceKeyAuthentication,
packetLimiterConfig
); );
} }
} }
@@ -987,4 +1002,35 @@ public class VelocityConfiguration implements ProxyConfig {
return enabled; 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
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
*/
public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
/**
* 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()),
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
);
} else {
return DEFAULT;
}
}
}
} }
@@ -28,7 +28,10 @@ public sealed interface ConfigurationMigration
permits ForwardingMigration, permits ForwardingMigration,
KeyAuthenticationMigration, KeyAuthenticationMigration,
MotdMigration, MotdMigration,
TransferIntegrationMigration { MiniMessageTranslationsMigration,
TransferIntegrationMigration,
PacketLimiterMigration,
PingPassthroughMigration {
boolean shouldMigrate(CommentedFileConfig config); boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException; void migrate(CommentedFileConfig config, Logger logger) throws IOException;
@@ -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);
}
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2026 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 static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import org.apache.logging.log4j.Logger;
/**
* Configuration migration for the new [packet-limiter] section.
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
*/
public final class PacketLimiterMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(CommentedFileConfig config) {
return configVersion(config) < 2.8;
}
@Override
public void migrate(CommentedFileConfig config, Logger logger) {
config.set("packet-limiter.interval", DEFAULT.interval());
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
config.setComment("packet-limiter.interval", """
Size of the moving time window in seconds used to calculate average rates.
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
config.setComment("packet-limiter.packets-per-second", """
Maximum average number of packets per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.bytes-per-second", """
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.decompressed-bytes-per-second", """
Maximum average number of decompressed bytes per second a client may send.
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
-1 disables this check.""");
config.set("config-version", "2.8");
}
}
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2024-2026 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.util.Locale;
import org.apache.logging.log4j.Logger;
/**
* Migrate the old ping passthrough entry to separate config entries.
*/
public final class PingPassthroughMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(final CommentedFileConfig config) {
return configVersion(config) < 2.9;
}
@Override
public void migrate(final CommentedFileConfig config, final Logger logger) {
// Get legacy ping passthrough value
final String legacyMode = config.getOrElse("ping-passthrough", "DISABLED")
.toUpperCase(Locale.ROOT);
boolean version = false;
boolean players = false;
boolean description = false;
boolean favicon = false;
boolean modinfo = false;
switch (legacyMode) {
case "ALL":
version = true;
players = true;
description = true;
favicon = true;
modinfo = true;
break;
case "DESCRIPTION":
description = true;
modinfo = true;
break;
case "MODS":
modinfo = true;
break;
default: // DISABLED
break;
}
config.removeComment("ping-passthrough");
config.remove("ping-passthrough");
config.setComment("announce-forge",
" Announce whether or not your server supports Forge. If you run a modded server, we\n"
+ " suggest turning this on.\n"
+ "\n"
+ " If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true\n"
+ " instead for a nicer display in the server list.");
config.set("ping-passthrough.version", version);
config.setComment(
"ping-passthrough.version",
" Should Velocity pass the version number from the backend server when responding to server list ping requests?"
);
config.set("ping-passthrough.players", players);
config.setComment(
"ping-passthrough.players",
" Should Velocity pass the player count from the backend server when responding to server list ping requests?"
);
config.set("ping-passthrough.description", description);
config.setComment(
"ping-passthrough.description",
" Should Velocity pass the description from the backend server when responding to server list ping requests?"
);
config.set("ping-passthrough.favicon", favicon);
config.setComment(
"ping-passthrough.favicon",
" Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?"
);
config.set("ping-passthrough.modinfo", modinfo);
config.setComment(
"ping-passthrough.modinfo",
" Should Velocity pass the mod list from the backend server when responding to server list ping requests?"
);
config.set("config-version", "2.9");
}
}
@@ -33,11 +33,14 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.natives.encryption.VelocityCipherFactory; import com.velocitypowered.natives.encryption.VelocityCipherFactory;
import com.velocitypowered.natives.util.Natives; import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.VelocityServer; 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.HandshakeSessionHandler;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.StatusSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.network.Connections;
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
@@ -66,7 +69,7 @@ import io.netty.util.ReferenceCountUtil;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.HashMap; import java.util.EnumMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -108,7 +111,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
this.server = server; this.server = server;
this.state = StateRegistry.HANDSHAKE; this.state = StateRegistry.HANDSHAKE;
this.sessionHandlers = new HashMap<>(); this.sessionHandlers = new EnumMap<>(StateRegistry.class);
} }
@Override @Override
@@ -153,13 +156,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (msg instanceof MinecraftPacket pkt) { if (msg instanceof MinecraftPacket pkt) {
if (!pkt.handle(activeSessionHandler)) { if (!pkt.handle(activeSessionHandler)) {
activeSessionHandler.handleGeneric((MinecraftPacket) msg); activeSessionHandler.handleGeneric(pkt);
} }
} else if (msg instanceof HAProxyMessage proxyMessage) { } else if (msg instanceof HAProxyMessage proxyMessage) {
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
proxyMessage.sourcePort()); proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) { } else if (msg instanceof ByteBuf buf) {
activeSessionHandler.handleUnknown((ByteBuf) msg); activeSessionHandler.handleUnknown(buf);
} }
} finally { } finally {
ReferenceCountUtil.release(msg); ReferenceCountUtil.release(msg);
@@ -368,6 +371,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setState(StateRegistry state) { public void setState(StateRegistry state) {
ensureInEventLoop(); ensureInEventLoop();
final StateRegistry previousState = this.state;
this.state = state; this.state = state;
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline() final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
.get(MinecraftVarintFrameDecoder.class); .get(MinecraftVarintFrameDecoder.class);
@@ -388,7 +392,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (state == StateRegistry.CONFIG) { if (state == StateRegistry.CONFIG) {
// Activate the play packet queue // Activate the play packet queue
addPlayPacketQueueHandler(); if (previousState == StateRegistry.PLAY
&& this.pendingConfigurationSwitch
&& this.association instanceof ConnectedPlayer) {
addPlayPacketQueueOutboundHandler();
} else {
addPlayPacketQueueHandler();
}
} else { } else {
// Remove the queue // Remove the queue
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
@@ -404,13 +414,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* Adds the play packet queue handler. * Adds the play packet queue handler.
*/ */
public void addPlayPacketQueueHandler() { public void addPlayPacketQueueHandler() {
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) { addPlayPacketQueueOutboundHandler();
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
}
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND, 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,14 +564,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
} else { } else {
int level = server.getConfiguration().getCompressionLevel(); int level = server.getConfiguration().getCompressionLevel();
VelocityCompressor compressor = Natives.compress.get().create(level); VelocityCompressor compressor = Natives.compress.get().create(level);
final MinecraftDecoder minecraftDecoder = (MinecraftDecoder) channel.pipeline().get(MINECRAFT_DECODER);
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor); encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
decoder = new MinecraftCompressDecoder(threshold, compressor); decoder = new MinecraftCompressDecoder(threshold, compressor, minecraftDecoder.getDirection());
channel.pipeline().remove(FRAME_ENCODER); channel.pipeline().remove(FRAME_ENCODER);
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
&& packetLimiterConfig.interval() > 0
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
}
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED); channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
} }
} }
@@ -23,7 +23,12 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket;
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.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.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
@@ -48,6 +53,8 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -67,6 +74,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.ActiveFeaturesPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; 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.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket; import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
@@ -193,6 +202,10 @@ public interface MinecraftSessionHandler {
return false; return false;
} }
default boolean handle(ServerboundPlayerLoadedPacket packet) {
return false;
}
default boolean handle(ServerLoginPacket packet) { default boolean handle(ServerLoginPacket packet) {
return false; return false;
} }
@@ -364,4 +377,36 @@ public interface MinecraftSessionHandler {
default boolean handle(ClientboundServerLinksPacket packet) { default boolean handle(ClientboundServerLinksPacket packet) {
return false; 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;
}
default boolean handle(ClientboundPostEffectsPacket 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.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; 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.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.command.CommandGraphInjector; import com.velocitypowered.proxy.command.CommandGraphInjector;
@@ -55,6 +57,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; 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.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
@@ -68,6 +71,7 @@ import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
@@ -91,6 +95,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
Boolean.getBoolean("velocity.log-server-backpressure"); Boolean.getBoolean("velocity.log-server-backpressure");
private static final int MAXIMUM_PACKETS_TO_FLUSH = private static final int MAXIMUM_PACKETS_TO_FLUSH =
Integer.getInteger("velocity.max-packets-per-flush", 8192); Integer.getInteger("velocity.max-packets-per-flush", 8192);
private static final int LARGE_PACKET_THRESHOLD = 1024 * 128;
private final VelocityServer server; private final VelocityServer server;
private final VelocityServerConnection serverConn; private final VelocityServerConnection serverConn;
@@ -162,6 +167,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return false; // forwards on return false; // forwards on
} }
@Override
public boolean handle(JoinGamePacket packet) {
// We receive this if the connection is reconfigured
packet.setOnlineMode(serverConn.getPlayer().isOnlineMode());
return false; // forward
}
@Override @Override
public boolean handle(ClientSettingsPacket packet) { public boolean handle(ClientSettingsPacket packet) {
serverConn.ensureConnected().write(packet); serverConn.ensureConnected().write(packet);
@@ -177,10 +189,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(BossBarPacket packet) { public boolean handle(BossBarPacket packet) {
if (packet.getAction() == BossBarPacket.ADD) { if (serverConn.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
playerSessionHandler.getServerBossBars().add(packet.getUuid()); if (packet.getAction() == BossBarPacket.ADD) {
} else if (packet.getAction() == BossBarPacket.REMOVE) { playerSessionHandler.getServerBossBars().add(packet.getUuid());
playerSessionHandler.getServerBossBars().remove(packet.getUuid()); } else if (packet.getAction() == BossBarPacket.REMOVE) {
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
}
} }
return false; // forward return false; // forward
} }
@@ -290,7 +304,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
// Register and unregister packets are simply forwarded to the server as-is. // Register and unregister packets are simply forwarded to the client as-is.
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) { if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
return false; return false;
} }
@@ -308,13 +322,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
return false;
}
byte[] copy = ByteBufUtil.getBytes(packet.content()); 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 -> { server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
PluginMessagePacket copied = new PluginMessagePacket( PluginMessagePacket copied = new PluginMessagePacket(
@@ -359,7 +369,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
// Inject commands from the proxy. // Inject commands from the proxy.
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector(); final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
injector.inject(rootNode, serverConn.getPlayer()); 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( server.getEventManager().fire(
@@ -445,11 +460,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void handleGeneric(MinecraftPacket packet) { public void handleGeneric(MinecraftPacket packet) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof PluginMessagePacket pluginMessage) {
((PluginMessagePacket) packet).retain(); pluginMessage.retain();
} }
boolean huge = packet instanceof DeferredByteBufHolder def && def.content().readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(packet); playerConnection.delayedWrite(packet);
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush(); playerConnection.flush();
packetsFlushed = 0; packetsFlushed = 0;
} }
@@ -457,8 +473,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void handleUnknown(ByteBuf buf) { public void handleUnknown(ByteBuf buf) {
boolean huge = buf.readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(buf.retain()); playerConnection.delayedWrite(buf.retain());
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush(); playerConnection.flush();
packetsFlushed = 0; packetsFlushed = 0;
} }
@@ -511,4 +528,4 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
playerConnection.setAutoReading(writable); playerConnection.setAutoReading(writable);
} }
} }
@@ -344,66 +344,30 @@ public class BungeeCordMessageResponder {
return false; return false;
} }
ByteBufDataInput in = new ByteBufDataInput(message.content()); final ByteBufDataInput in = new ByteBufDataInput(message.content());
String subChannel = in.readUTF(); final String subChannel = in.readUTF();
switch (subChannel) { switch (subChannel) {
case "GetPlayerServer": case "GetPlayerServer" -> this.processGetPlayerServer(in);
this.processGetPlayerServer(in); case "ForwardToPlayer" -> this.processForwardToPlayer(in);
break; case "Forward" -> this.processForwardToServer(in);
case "ForwardToPlayer": case "Connect" -> this.processConnect(in);
this.processForwardToPlayer(in); case "ConnectOther" -> this.processConnectOther(in);
break; case "IP" -> this.processIp(in);
case "Forward": case "PlayerCount" -> this.processPlayerCount(in);
this.processForwardToServer(in); case "PlayerList" -> this.processPlayerList(in);
break; case "GetServers" -> this.processGetServers();
case "Connect": case "Message" -> this.processMessage(in);
this.processConnect(in); case "MessageRaw" -> this.processMessageRaw(in);
break; case "GetServer" -> this.processGetServer();
case "ConnectOther": case "UUID" -> this.processUuid();
this.processConnectOther(in); case "UUIDOther" -> this.processUuidOther(in);
break; case "IPOther" -> this.processIpOther(in);
case "IP": case "ServerIP" -> this.processServerIp(in);
this.processIp(in); case "KickPlayer" -> this.processKick(in);
break; case "KickPlayerRaw" -> this.processKickRaw(in);
case "PlayerCount": default -> {
this.processPlayerCount(in); // Do nothing, unknown command
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;
} }
return true; 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.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; 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.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket; import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; 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 com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.io.IOException; import io.netty.channel.Channel;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.key.Key; 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. * 1.20.2+ switching. Yes, some of this is exceptionally stupid.
*/ */
public class ConfigSessionHandler implements MinecraftSessionHandler { 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 static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class);
private final VelocityServer server; private final VelocityServer server;
private final VelocityServerConnection serverConn; private final VelocityServerConnection serverConn;
@@ -256,7 +260,13 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(DisconnectPacket packet) { public boolean handle(DisconnectPacket packet) {
serverConn.disconnect(); 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; return true;
} }
@@ -267,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion())); serverConn.getPlayer().getProtocolVersion()));
} else { } else {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -277,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
this.serverConn.getConnection().setAutoReading(false); this.serverConn.getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes)) .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
@@ -358,10 +368,16 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
@Override
public boolean handle(CodeOfConductPacket packet) {
this.serverConn.getPlayer().getConnection().write(packet.retain());
return true;
}
@Override @Override
public void disconnected() { public void disconnected() {
resultFuture.completeExceptionally( resultFuture.complete(ConnectionRequestResults.forDisconnect(
new IOException("Unexpectedly disconnected from remote server")); ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
} }
@Override @Override
@@ -369,6 +385,22 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
serverConn.getPlayer().getConnection().write(packet); 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) { private void switchFailure(Throwable cause) {
logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(),
serverConn.getPlayer().getUsername(), cause); serverConn.getPlayer().getUsername(), cause);
@@ -382,4 +414,4 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
public enum State { public enum State {
START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE 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) { if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
smc.setAutoReading(false); smc.setAutoReading(false);
clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop()); clientPlaySessionHandler.doSwitch().thenRunAsync(() -> smc.setAutoReading(true), smc.eventLoop());
} else { } else {
// Initial login - the player is already in configuration state. // Initial login - the player is already in configuration state.
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn)); 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.JoinGamePacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import java.io.IOException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -90,7 +89,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(JoinGamePacket packet) { public boolean handle(JoinGamePacket packet) {
MinecraftConnection smc = serverConn.ensureConnected(); final MinecraftConnection smc = serverConn.ensureConnected();
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null); final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
final ConnectedPlayer player = serverConn.getPlayer(); final ConnectedPlayer player = serverConn.getPlayer();
final VelocityServerConnection existingConnection = player.getConnectedServer(); final VelocityServerConnection existingConnection = player.getConnectedServer();
@@ -107,6 +106,9 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// Reset Tablist header and footer to prevent desync // Reset Tablist header and footer to prevent desync
player.clearPlayerListHeaderAndFooter(); player.clearPlayerListHeaderAndFooter();
// Override online mode
packet.setOnlineMode(player.isOnlineMode());
// The goods are in hand! We got JoinGame. Let's transition completely to the new state. // The goods are in hand! We got JoinGame. Let's transition completely to the new state.
smc.setAutoReading(false); smc.setAutoReading(false);
server.getEventManager() server.getEventManager()
@@ -122,9 +124,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// Change the client to use the ClientPlaySessionHandler if required. // Change the client to use the ClientPlaySessionHandler if required.
ClientPlaySessionHandler playHandler; ClientPlaySessionHandler playHandler;
if (player.getConnection() if (player.getConnection()
.getActiveSessionHandler() instanceof ClientPlaySessionHandler) { .getActiveSessionHandler() instanceof ClientPlaySessionHandler sessionHandler) {
playHandler = playHandler = sessionHandler;
(ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler();
} else { } else {
playHandler = new ClientPlaySessionHandler(server, player); playHandler = new ClientPlaySessionHandler(server, player);
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler); player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler);
@@ -214,7 +215,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void disconnected() { public void disconnected() {
resultFuture resultFuture.complete(ConnectionRequestResults.forDisconnect(
.completeExceptionally(new IOException("Unexpectedly disconnected from remote server")); ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
} }
} }
@@ -53,6 +53,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -67,9 +68,11 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final VelocityServer server; private final VelocityServer server;
private @Nullable MinecraftConnection connection; private @Nullable MinecraftConnection connection;
private boolean hasCompletedJoin = false; private boolean hasCompletedJoin = false;
private boolean clientLoaded = false; // 1.21.4+
private boolean gracefulDisconnect = false; private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>(); private final Map<Long, Long> pendingPings = new HashMap<>();
private @MonotonicNonNull Integer entityId;
/** /**
* Initializes a new server connection. * Initializes a new server connection.
@@ -178,9 +181,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setServerAddress(createBungeeGuardForwardingAddress(secret)); handshake.setServerAddress(createBungeeGuardForwardingAddress(secret));
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) { } else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN); handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN);
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType) { } else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType forgeConnection) {
handshake.setServerAddress(playerVhost + ((ModernForgeConnectionType) proxyPlayer handshake.setServerAddress(playerVhost + forgeConnection.getModernToken());
.getConnection().getType()).getModernToken());
} else { } else {
handshake.setServerAddress(playerVhost); handshake.setServerAddress(playerVhost);
} }
@@ -316,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
} }
} }
public void setClientLoaded(boolean clientLoaded) {
this.clientLoaded = clientLoaded;
}
@Override
public boolean isClientLoaded() {
return clientLoaded;
}
boolean isGracefulDisconnect() { boolean isGracefulDisconnect() {
return gracefulDisconnect; return gracefulDisconnect;
} }
@@ -324,6 +335,14 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
return pendingPings; 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 * 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. * 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 @MonotonicNonNull ConnectedPlayer connectedPlayer;
private final boolean onlineMode; private final boolean onlineMode;
private State loginState = State.START; // 1.20.2+ private State loginState = State.START; // 1.20.2+
private final String serverIdHash;
AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound,
GameProfile profile, boolean onlineMode) { GameProfile profile, boolean onlineMode, String serverIdHash) {
this.server = Preconditions.checkNotNull(server, "server"); this.server = Preconditions.checkNotNull(server, "server");
this.inbound = Preconditions.checkNotNull(inbound, "inbound"); this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.profile = Preconditions.checkNotNull(profile, "profile"); this.profile = Preconditions.checkNotNull(profile, "profile");
this.onlineMode = onlineMode; this.onlineMode = onlineMode;
this.mcConnection = inbound.delegatedConnection(); this.mcConnection = inbound.delegatedConnection();
this.serverIdHash = serverIdHash;
} }
@Override @Override
@@ -213,7 +215,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) {
mcConnection.setAssociation(player); mcConnection.setAssociation(player);
server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> { server.getEventManager().fire(new LoginEvent(player, serverIdHash)).thenAcceptAsync(event -> {
if (mcConnection.isClosed()) { if (mcConnection.isClosed()) {
// The player was disconnected // The player was disconnected
server.getEventManager().fireAndForget(new DisconnectEvent(player, server.getEventManager().fireAndForget(new DisconnectEvent(player,
@@ -234,6 +236,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
success.setUsername(player.getUsername()); success.setUsername(player.getUsername());
success.setProperties(player.getGameProfileProperties()); success.setProperties(player.getGameProfileProperties());
success.setUuid(player.getUniqueId()); success.setUuid(player.getUniqueId());
if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
success.setSessionId(server.getSessionId());
}
mcConnection.write(success); mcConnection.write(success);
loginState = State.SUCCESS_SENT; loginState = State.SUCCESS_SENT;
@@ -33,6 +33,7 @@ import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResp
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; 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.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
@@ -40,10 +41,13 @@ import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; 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.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -58,6 +62,8 @@ import org.apache.logging.log4j.Logger;
* Handles the client config stage. * Handles the client config stage.
*/ */
public class ClientConfigSessionHandler implements MinecraftSessionHandler { 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 static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class);
private final VelocityServer server; private final VelocityServer server;
@@ -130,7 +136,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
return true; return true;
} else if (serverConn != null) { } else if (serverConn != null) {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -140,6 +145,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
serverConn.getPlayer().getConnection().setAutoReading(false); serverConn.getPlayer().getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes)) .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
@@ -205,6 +211,27 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
@Override
public boolean handle(ServerboundCustomClickActionPacket packet) {
VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer();
if (serverConnection != null) {
serverConnection.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 @Override
public void handleGeneric(MinecraftPacket packet) { public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer(); VelocityServerConnection serverConnection = player.getConnectedServer();
@@ -215,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) { if (smc != null && serverConnection.getPhase().consideredComplete()) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof ByteBufHolder bufHolder) {
((PluginMessagePacket) packet).retain(); bufHolder.retain();
} }
smc.write(packet); smc.write(packet);
} }
@@ -244,6 +271,36 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void exception(Throwable throwable) { 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() {
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,18 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestion;
import com.velocitypowered.api.command.VelocityBrigadierMessage;
import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; 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.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; 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.VelocityServer;
import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.ConnectionTypes;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
@@ -41,6 +44,7 @@ import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry; 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.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket; import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
@@ -49,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.RespawnPacket; import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
@@ -85,8 +90,11 @@ import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -97,6 +105,16 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* center that joins backend servers with players. * center that joins backend servers with players.
*/ */
public class ClientPlaySessionHandler implements MinecraftSessionHandler { public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
// Caps the per-connection queue used while the FML/login phases are not yet "complete". Without
// these caps, a client that never completes its handshake phase can spam plugin messages (each up
// to ~32 KiB serverbound) and grow the queue without bound.
private static final long MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES =
Long.getLong("velocity.max-queued-login-plugin-message-bytes", 4L * 1024 * 1024);
private static final int MAX_QUEUED_LOGIN_PLUGIN_MESSAGES =
Integer.getInteger("velocity.max-queued-login-plugin-messages", 1024);
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class); private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
@@ -104,6 +122,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private boolean spawned = false; private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>(); private final List<UUID> serverBossBars = new ArrayList<>();
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>(); private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
private final AtomicLong loginPluginMessagesBytes = new AtomicLong();
private final AtomicInteger loginPluginMessagesCount = new AtomicInteger();
private volatile boolean loginPluginMessagesOverflowed;
private final VelocityServer server; private final VelocityServer server;
private @Nullable TabCompleteRequestPacket outstandingTabComplete; private @Nullable TabCompleteRequestPacket outstandingTabComplete;
private final ChatHandler<? extends MinecraftPacket> chatHandler; private final ChatHandler<? extends MinecraftPacket> chatHandler;
@@ -172,9 +193,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void deactivated() { public void deactivated() {
player.discardChatQueue(); player.discardChatQueue();
for (PluginMessagePacket message : loginPluginMessages) { PluginMessagePacket message;
while ((message = loginPluginMessages.poll()) != null) {
ReferenceCountUtil.release(message); ReferenceCountUtil.release(message);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
}
/**
* Adds a retained plugin message to the queue used while the FML/login phases are still in
* progress, enforcing the per-connection byte and count caps. Returns {@code true} if queued,
* {@code false} if the packet was released (and the player disconnected on overflow).
*/
private boolean enqueueLoginPluginMessage(PluginMessagePacket packet) {
if (loginPluginMessagesOverflowed) {
ReferenceCountUtil.release(packet);
return false;
}
int packetSize = packet.content().readableBytes();
long newBytes = loginPluginMessagesBytes.addAndGet(packetSize);
int newCount = loginPluginMessagesCount.incrementAndGet();
if (newBytes > MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES
|| newCount > MAX_QUEUED_LOGIN_PLUGIN_MESSAGES) {
loginPluginMessagesOverflowed = true;
ReferenceCountUtil.release(packet);
logger.warn("Disconnecting {}: pre-join plugin-message queue exceeded its limits "
+ "({} messages, {} bytes).", player, newCount, newBytes);
player.disconnect(Component.translatable("velocity.error.plugin-message-overflow"));
return false;
}
loginPluginMessages.add(packet);
return true;
} }
@Override @Override
@@ -195,6 +245,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true; // will forward onto the server return true; // will forward onto the server
} }
@Override
public boolean handle(ServerboundPlayerLoadedPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
return true;
}
if (!serverConnection.isClientLoaded()) {
serverConnection.setClientLoaded(true);
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
}
return false;
}
@Override @Override
public boolean handle(SessionPlayerCommandPacket packet) { public boolean handle(SessionPlayerCommandPacket packet) {
if (player.getCurrentServer().isEmpty()) { if (player.getCurrentServer().isEmpty()) {
@@ -318,8 +382,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels))); new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain()); backendConn.write(packet.retain());
} else if (PluginMessageUtil.isUnregister(packet)) { } else if (PluginMessageUtil.isUnregister(packet)) {
player.getClientsideChannels() List<ChannelIdentifier> channels =
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion())); PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion());
player.getClientsideChannels().removeAll(channels);
server.getEventManager()
.fireAndForget(
new PlayerChannelUnregisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain()); backendConn.write(packet.retain());
} else if (PluginMessageUtil.isMcBrand(packet)) { } else if (PluginMessageUtil.isMcBrand(packet)) {
String brand = PluginMessageUtil.readBrandMessage(packet.content()); String brand = PluginMessageUtil.readBrandMessage(packet.content());
@@ -351,7 +419,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// //
// We also need to make sure to retain these packets, so they can be flushed // We also need to make sure to retain these packets, so they can be flushed
// appropriately. // appropriately.
loginPluginMessages.add(packet.retain()); enqueueLoginPluginMessage(packet.retain());
} else { } else {
// The connection is ready, send the packet now. // The connection is ready, send the packet now.
backendConn.write(packet.retain()); backendConn.write(packet.retain());
@@ -366,7 +434,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (!player.getPhase().consideredComplete() || !serverConn.getPhase() if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) { .consideredComplete()) {
// We're still processing the connection (see above), enqueue the packet for now. // We're still processing the connection (see above), enqueue the packet for now.
loginPluginMessages.add(message.retain()); enqueueLoginPluginMessage(message.retain());
} else { } else {
backendConn.write(message); backendConn.write(message);
} }
@@ -463,7 +531,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); 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) { if (packet instanceof PluginMessagePacket) {
((PluginMessagePacket) packet).retain(); ((PluginMessagePacket) packet).retain();
} }
@@ -480,7 +552,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); 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()); smc.write(buf.retain());
} }
} }
@@ -492,14 +568,24 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void exception(Throwable throwable) { public void exception(Throwable throwable) {
player.disconnect( player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); if (MinecraftDecoder.DEBUG) {
logger.info("Exception while handling packet for {}", player, throwable);
}
} }
@Override @Override
public void writabilityChanged() { public void writabilityChanged() {
boolean writable = player.getConnection().getChannel().isWritable(); 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) { if (!writable) {
// We might have packets queued from the server, so flush them now to free up memory. Make // 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 // sure to do it on a future invocation of the event loop, otherwise while the issue will
@@ -535,9 +621,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Config state clears everything in the client. No need to clear later. // Config state clears everything in the client. No need to clear later.
spawned = false; spawned = false;
serverBossBars.clear();
player.clearPlayerListHeaderAndFooterSilent(); player.clearPlayerListHeaderAndFooterSilent();
player.getTabList().clearAllSilent(); player.getTabList().clearAllSilent();
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
player.getBossBarManager().dropPackets();
} else {
serverBossBars.clear();
}
} }
player.switchToConfigState(); player.switchToConfigState();
@@ -575,15 +665,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
} }
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to destination.setEntityId(joinGame.getEntityId()); // used for sound api
// track them. if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
for (UUID serverBossBar : serverBossBars) { player.getBossBarManager().sendBossBars();
BossBarPacket deletePacket = new BossBarPacket(); } else {
deletePacket.setUuid(serverBossBar); // Remove previous boss bars. These don't get cleared when sending JoinGame (up until 1.20.2),
deletePacket.setAction(BossBarPacket.REMOVE); // thus the need to track them.
player.getConnection().delayedWrite(deletePacket); 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. // Tell the server about the proxy's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion(); ProtocolVersion serverVersion = serverMc.getProtocolVersion();
@@ -602,6 +697,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
while ((pm = loginPluginMessages.poll()) != null) { while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm); serverMc.delayedWrite(pm);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
// Clear any title from the previous server. // Clear any title from the previous server.
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
@@ -694,23 +791,35 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return; return;
} }
List<Offer> offers = new ArrayList<>(); int startPos = -1;
for (Suggestion suggestion : suggestions.getList()) { for (var suggestion : suggestions.getList()) {
String offer = suggestion.getText(); if (startPos == -1 || startPos > suggestion.getRange().getStart()) {
ComponentHolder tooltip = null; startPos = suggestion.getRange().getStart();
if (suggestion.getTooltip() != null
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
tooltip = new ComponentHolder(player.getProtocolVersion(),
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
} }
offers.add(new Offer(offer, tooltip));
} }
int startPos = packet.getCommand().lastIndexOf(' ') + 1;
if (startPos > 0) { 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(); TabCompleteResponsePacket resp = new TabCompleteResponsePacket();
resp.setTransactionId(packet.getTransactionId()); resp.setTransactionId(packet.getTransactionId());
resp.setStart(startPos); resp.setStart(startPos + 1);
resp.setLength(packet.getCommand().length() - startPos); resp.setLength(packet.getCommand().length() - startPos - 1);
resp.getOffers().addAll(offers); resp.getOffers().addAll(offers);
player.getConnection().write(resp); player.getConnection().write(resp);
} }
@@ -765,10 +874,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
offer = offer.substring(command.length()); offer = offer.substring(command.length());
} }
ComponentHolder tooltip = null; ComponentHolder tooltip = null;
if (suggestion.getTooltip() != null if (suggestion.getTooltip() instanceof ComponentLike componentLike) {
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) { tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent());
tooltip = new ComponentHolder(player.getProtocolVersion(), } else if (suggestion.getTooltip() != null) {
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent()); tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString()));
} }
response.getOffers().add(new Offer(offer, tooltip)); response.getOffers().add(new Offer(offer, tooltip));
} }
@@ -822,6 +931,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
while ((pm = loginPluginMessages.poll()) != null) { while ((pm = loginPluginMessages.poll()) != null) {
connection.write(pm); connection.write(pm);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
} }
} }
} }
@@ -62,6 +62,7 @@ import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; 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.bundle.BundleDelimiterHandler;
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; 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.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; 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.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
@@ -81,7 +84,6 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket; import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
@@ -108,6 +110,7 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@@ -115,23 +118,24 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.permission.PermissionChecker; 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.Pointers;
import net.kyori.adventure.pointer.PointersSupplier; import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.resource.ResourcePackInfoLike; import net.kyori.adventure.resource.ResourcePackInfoLike;
import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike; 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.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger; 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.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.Title.Times; import net.kyori.adventure.title.Title.Times;
import net.kyori.adventure.title.TitlePart; import net.kyori.adventure.title.TitlePart;
import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.GlobalTranslator;
@@ -160,7 +164,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername())) .resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
.resolving(Identity.LOCALE, Player::getEffectiveLocale) .resolving(Identity.LOCALE, Player::getEffectiveLocale)
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker) .resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
.resolving(FacetPointers.TYPE, player -> Type.PLAYER)
.build(); .build();
/** /**
@@ -197,6 +200,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private @Nullable ClientSettingsPacket clientSettingsPacket; private @Nullable ClientSettingsPacket clientSettingsPacket;
private volatile ChatQueue chatQueue; private volatile ChatQueue chatQueue;
private final ChatBuilderFactory chatBuilderFactory; private final ChatBuilderFactory chatBuilderFactory;
private final BossBarManager bossBarManager;
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode, @Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
@@ -223,6 +227,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.chatQueue = new ChatQueue(this); this.chatQueue = new ChatQueue(this);
this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion()); this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion());
this.resourcePackHandler = ResourcePackHandler.create(this, server); this.resourcePackHandler = ResourcePackHandler.create(this, server);
this.bossBarManager = new BossBarManager(this);
} }
/** /**
@@ -416,29 +421,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message) { public void sendMessage(final @NonNull Component message) {
Preconditions.checkNotNull(message, "message");
final Component translated = translateMessage(message); final Component translated = translateMessage(message);
connection.write(getChatBuilderFactory().builder() connection.write(getChatBuilderFactory().builder()
.component(translated).forIdentity(identity).toClient()); .component(translated).toClient());
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message, public void sendActionBar(@NonNull Component message) {
@NonNull MessageType type) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkNotNull(type, "type");
Component translated = translateMessage(message);
connection.write(getChatBuilderFactory().builder()
.component(translated).forIdentity(identity)
.setType(type == MessageType.CHAT ? ChatType.CHAT : ChatType.SYSTEM)
.toClient());
}
@Override
public void sendActionBar(net.kyori.adventure.text.@NonNull Component message) {
Component translated = translateMessage(message); Component translated = translateMessage(message);
ProtocolVersion playerVersion = getProtocolVersion(); ProtocolVersion playerVersion = getProtocolVersion();
@@ -495,11 +487,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void showTitle(net.kyori.adventure.title.@NonNull Title title) { public void showTitle(@NonNull Title title) {
if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket( GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion()); GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion());
net.kyori.adventure.title.Title.Times times = title.times(); Title.Times times = title.times();
if (times != null) { if (times != null) {
timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn())); timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn()));
timesPkt.setStay((int) DurationUtils.toTicks(times.stay())); timesPkt.setStay((int) DurationUtils.toTicks(times.stay()));
@@ -637,7 +629,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void disconnect(Component reason) { public void disconnect(@NotNull Component reason) {
Objects.requireNonNull(reason, "reason");
if (connection.eventLoop().inEventLoop()) { if (connection.eventLoop().inEventLoop()) {
disconnect0(reason, false); disconnect0(reason, false);
} else { } else {
@@ -706,12 +699,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component friendlyError; Component friendlyError;
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
friendlyError = Component.translatable("velocity.error.connected-server-error", friendlyError = Component.translatable("velocity.error.connected-server-error",
Component.text(server.getServerInfo().getName())); Argument.string("server", server.getServerInfo().getName()));
} else { } else {
logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(), logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(),
wrapped); wrapped);
friendlyError = Component.translatable("velocity.error.connecting-server-error", 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); handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
} }
@@ -733,18 +726,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component disconnectReason = disconnect.getReason().getComponent(); Component disconnectReason = disconnect.getReason().getComponent();
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason); String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(), if (this.server.getConfiguration().isLogPlayerConnections()) {
plainTextReason); logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
plainTextReason);
}
handleConnectionException(server, disconnectReason, handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED, Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED,
Component.text(server.getServerInfo().getName()), Argument.string("server", server.getServerInfo().getName()),
disconnectReason), safe); disconnectReason), safe);
} else { } else {
logger.error("{}: disconnected while connecting to {}: {}", this, if (this.server.getConfiguration().isLogPlayerConnections()) {
server.getServerInfo().getName(), plainTextReason); logger.error("{}: disconnected while connecting to {}: {}", this,
server.getServerInfo().getName(), plainTextReason);
}
handleConnectionException(server, disconnectReason, handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED, Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
Component.text(server.getServerInfo().getName()), Argument.string("server", server.getServerInfo().getName()),
disconnectReason), safe); disconnectReason), safe);
} }
} }
@@ -800,63 +797,56 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return; return;
} }
if (event.getResult() instanceof final DisconnectPlayer res) { switch (event.getResult()) {
disconnect(res.getReasonComponent()); case DisconnectPlayer res -> disconnect(res.getReasonComponent());
} else if (event.getResult() instanceof final RedirectPlayer res) { case RedirectPlayer res -> createConnectionRequest(res.getServer(), previousConnection).connect()
createConnectionRequest(res.getServer(), previousConnection).connect() .whenCompleteAsync((status, throwable) -> {
.whenCompleteAsync((status, throwable) -> { if (throwable != null) {
if (throwable != null) { handleConnectionException(res.getServer(), throwable, true);
handleConnectionException( return;
status != null ? status.getAttemptedConnection() : res.getServer(), throwable, }
true);
return;
}
switch (status.getStatus()) { switch (status.getStatus()) {
// Impossible/nonsensical cases // Impossible/nonsensical cases
case ALREADY_CONNECTED: case ALREADY_CONNECTED -> logger.error("{}: already connected to {}", this,
logger.error("{}: already connected to {}", this, status.getAttemptedConnection().getServerInfo().getName());
status.getAttemptedConnection().getServerInfo().getName()); case CONNECTION_IN_PROGRESS, CONNECTION_CANCELLED -> {
break; Component fallbackMsg = res.getMessageComponent();
case CONNECTION_IN_PROGRESS: if (fallbackMsg == null) {
// Fatal case fallbackMsg = friendlyReason;
case CONNECTION_CANCELLED: }
Component fallbackMsg = res.getMessageComponent(); disconnect(status.getReasonComponent().orElse(fallbackMsg));
if (fallbackMsg == null) { }
fallbackMsg = friendlyReason; 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)); }, connection.eventLoop());
break; case Notify res -> {
case SERVER_DISCONNECTED: if (event.kickedDuringServerConnect() && previousConnection != null) {
Component reason = status.getReasonComponent() sendMessage(res.getMessageComponent());
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); } else {
handleConnectionException(res.getServer(), disconnect(res.getMessageComponent());
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());
} }
} else {
// In case someone gets creative, assume we want to disconnect the player. // In case someone gets creative, assume we want to disconnect the player.
disconnect(friendlyReason); default -> disconnect(friendlyReason);
} }
}, connection.eventLoop()); }, connection.eventLoop());
} }
@@ -1038,6 +1028,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.clientBrand = clientBrand; 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 @Override
public void transferToHost(final InetSocketAddress address) { public void transferToHost(final InetSocketAddress address) {
Preconditions.checkNotNull(address); Preconditions.checkNotNull(address);
@@ -1300,11 +1334,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
if (sentTime != null) { if (sentTime != null) {
final MinecraftConnection smc = serverConnection.getConnection(); 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)); setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
smc.write(packet); smc.write(packet);
return true;
} }
// We removed this, and so this is ours
return true;
} }
} }
return false; return false;
@@ -1314,7 +1354,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
* Switches the connection to the client into config state. * Switches the connection to the client into config state.
*/ */
public void switchToConfigState() { 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(() -> { .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
// if the connection was closed earlier, there is a risk that the player is no longer connected // if the connection was closed earlier, there is a risk that the player is no longer connected
if (!connection.getChannel().isActive()) { if (!connection.getChannel().isActive()) {
@@ -1329,7 +1370,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
connection.pendingConfigurationSwitch = true; connection.pendingConfigurationSwitch = true;
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
// Make sure we don't send any play packets to the player after update start // Make sure we don't send any play packets to the player after update start
connection.addPlayPacketQueueHandler(); connection.addPlayPacketQueueOutboundHandler();
}, connection.eventLoop()).exceptionally((ex) -> { }, connection.eventLoop()).exceptionally((ex) -> {
logger.error("Error switching player connection to config state", ex); logger.error("Error switching player connection to config state", ex);
return null; return null;
@@ -1379,6 +1420,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return handshakeIntent; return handshakeIntent;
} }
public BossBarManager getBossBarManager() {
return bossBarManager;
}
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect; private final RegisteredServer toConnect;
@@ -1438,7 +1483,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
VelocityServerConnection con = VelocityServerConnection con =
new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server); new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server);
connectionInFlight = con; 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());
}, connection.eventLoop()); }, connection.eventLoop());
}); });
@@ -1452,22 +1506,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
@Override @Override
public CompletableFuture<Result> connect() { public CompletableFuture<Result> connect() {
return this.internalConnect().whenCompleteAsync((status, throwable) -> { return this.internalConnect().thenApply(x -> x);
if (status != null && !status.isSuccessful()) {
if (!status.isSafe()) {
handleConnectionException(status.getAttemptedConnection(), throwable, false);
}
}
}, connection.eventLoop()).thenApply(x -> x);
} }
@Override @Override
public CompletableFuture<Boolean> connectWithIndication() { public CompletableFuture<Boolean> connectWithIndication() {
return internalConnect().whenCompleteAsync((status, throwable) -> { return internalConnect().whenCompleteAsync((status, throwable) -> {
if (throwable != null) { if (throwable != null) {
// TODO: The exception handling from this is not very good. Find a better way. handleConnectionException(toConnect, throwable, true);
handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect,
throwable, true);
return; return;
} }
@@ -127,10 +127,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
if (!handshake.getProtocolVersion().isSupported()) { if (!handshake.getProtocolVersion().isSupported()) {
// Bump connection into correct protocol state so that we can send the disconnect packet. // Bump connection into correct protocol state so that we can send the disconnect packet.
connection.setState(StateRegistry.LOGIN); connection.setState(StateRegistry.LOGIN);
ic.disconnectQuietly(Component.translatable() ic.disconnectQuietly(Component.translatable(
.key("multiplayer.disconnect.outdated_client") "multiplayer.disconnect.outdated_client",
.arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)) Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)
.build()); ));
return; return;
} }
@@ -65,8 +65,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
} }
byte[] copy = ByteBufUtil.getBytes(packet.content()); byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
copy);
server.getEventManager().fire(event) server.getEventManager().fire(event)
.thenAcceptAsync(pme -> { .thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && serverConn.isActive()) { if (pme.getResult().isAllowed() && serverConn.isActive()) {
@@ -50,9 +50,9 @@ import java.net.http.HttpResponse;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -65,6 +65,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class InitialLoginSessionHandler implements MinecraftSessionHandler { public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class); private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final String MOJANG_HASJOINED_URL = private static final String MOJANG_HASJOINED_URL =
System.getProperty("mojang.sessionserver", System.getProperty("mojang.sessionserver",
"https://sessionserver.mojang.com/session/minecraft/hasJoined") "https://sessionserver.mojang.com/session/minecraft/hasJoined")
@@ -152,7 +153,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} else { } else {
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, new AuthSessionHandler(server, inbound,
GameProfile.forOfflinePlayer(login.getUsername()), false)); GameProfile.forOfflinePlayer(login.getUsername()), false, null));
} }
}); });
}); });
@@ -199,8 +200,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
mcConnection.enableEncryption(decryptedSharedSecret);
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
String url = String.format(MOJANG_HASJOINED_URL, String url = String.format(MOJANG_HASJOINED_URL,
urlFormParameterEscaper().escape(login.getUsername()), serverId); urlFormParameterEscaper().escape(login.getUsername()), serverId);
@@ -214,6 +219,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
server.getVersion().getName() + "/" + server.getVersion().getVersion()) server.getVersion().getName() + "/" + server.getVersion().getVersion())
.uri(URI.create(url)) .uri(URI.create(url))
.build(); .build();
//noinspection resource
final HttpClient httpClient = server.createHttpClient(); final HttpClient httpClient = server.createHttpClient();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.whenCompleteAsync((response, throwable) -> { .whenCompleteAsync((response, throwable) -> {
@@ -228,18 +234,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
return; return;
} }
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption for connection", e);
// At this point, the connection is encrypted, but something's wrong on our side and
// we can't do anything about it.
mcConnection.close(true);
return;
}
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
final GameProfile profile = GENERAL_GSON.fromJson(response.body(), final GameProfile profile = GENERAL_GSON.fromJson(response.body(),
GameProfile.class); GameProfile.class);
@@ -254,7 +248,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
// All went well, initialize the session. // All went well, initialize the session.
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, profile, true)); new AuthSessionHandler(server, inbound, profile, true, serverId));
} else if (response.statusCode() == 204) { } else if (response.statusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy. // Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect( inbound.disconnect(
@@ -267,16 +261,8 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down")); inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
} }
}, mcConnection.eventLoop()) }, mcConnection.eventLoop())
.thenRun(() -> { .whenComplete((ignored, throwable) -> {
if (httpClient instanceof final AutoCloseable closeable) { httpClient.close();
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);
}
}
}); });
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e); logger.error("Unable to enable encryption", e);
@@ -287,7 +273,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private EncryptionRequestPacket generateEncryptionRequest() { private EncryptionRequestPacket generateEncryptionRequest() {
byte[] verify = new byte[4]; byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify); SECURE_RANDOM.nextBytes(verify);
EncryptionRequestPacket request = new EncryptionRequestPacket(); EncryptionRequestPacket request = new EncryptionRequestPacket();
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded()); request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
@@ -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;
}
}
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* Modern (Minecraft 1.20.3+) ResourcePackHandler * Modern (Minecraft 1.20.3+) ResourcePackHandler.
*/ */
public final class ModernResourcePackHandler extends ResourcePackHandler { public final class ModernResourcePackHandler extends ResourcePackHandler {
private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks = private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks =
@@ -94,7 +94,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
this.outstandingResourcePacks.get(info.getId()); this.outstandingResourcePacks.get(info.getId());
outstandingResourcePacks.add(info); outstandingResourcePacks.add(info);
if (outstandingResourcePacks.size() == 1) { 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 = final List<ResourcePackInfo> outstandingResourcePacks =
this.outstandingResourcePacks.get(uuid); this.outstandingResourcePacks.get(uuid);
if (!outstandingResourcePacks.isEmpty()) { if (!outstandingResourcePacks.isEmpty()) {
sendResourcePackRequestPacket(outstandingResourcePacks.get(0)); sendResourcePackRequestPacket(outstandingResourcePacks.getFirst());
} }
} }
@@ -124,7 +124,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
this.outstandingResourcePacks.get(uuid); this.outstandingResourcePacks.get(uuid);
final boolean peek = bundle.status().isIntermediate(); final boolean peek = bundle.status().isIntermediate();
final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null : final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null :
peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0); peek ? outstandingResourcePacks.getFirst() : outstandingResourcePacks.removeFirst();
server.getEventManager() server.getEventManager()
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued)) .fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
@@ -111,13 +111,14 @@ public abstract sealed class ResourcePackHandler
} }
request.setRequired(queued.getShouldForce()); request.setRequired(queued.getShouldForce());
request.setPrompt(queued.getPrompt() == null ? null : request.setPrompt(queued.getPrompt() == null ? null :
new ComponentHolder(player.getProtocolVersion(), queued.getPrompt())); new ComponentHolder(player.getProtocolVersion(), player.translateMessage(queued.getPrompt())));
player.getConnection().write(request); player.getConnection().write(request);
} }
/** /**
* Processes a client response to a sent resource-pack. * Processes a client response to a sent resource-pack.
*
* <p>Cases in which no action will be taken:</p> * <p>Cases in which no action will be taken:</p>
* <ul> * <ul>
* *
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.server.PingOptions; import com.velocitypowered.api.proxy.server.PingOptions;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PingPassthroughMode; import com.velocitypowered.proxy.config.PingPassthroughMode;
@@ -36,6 +37,7 @@ import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
/** /**
* Common utilities for handling server list ping results. * Common utilities for handling server list ping results.
@@ -56,16 +58,16 @@ public class ServerListPingHandler {
List<ServerPing.SamplePlayer> samplePlayers; List<ServerPing.SamplePlayer> samplePlayers;
if (configuration.getSamplePlayersInPing()) { if (configuration.getSamplePlayersInPing()) {
List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream() List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream()
.map(p -> { .map(p -> {
if (p.getPlayerSettings().isClientListingAllowed()) { if (p.getPlayerSettings().isClientListingAllowed()) {
return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId()); return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId());
} else { } else {
return ServerPing.SamplePlayer.ANONYMOUS; return ServerPing.SamplePlayer.ANONYMOUS;
} }
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.shuffle(unshuffledPlayers); Collections.shuffle(unshuffledPlayers);
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount())); samplePlayers = unshuffledPlayers.subList(0, Math.min(12, unshuffledPlayers.size()));
} else { } else {
samplePlayers = ImmutableList.of(); samplePlayers = ImmutableList.of();
} }
@@ -99,58 +101,68 @@ public class ServerListPingHandler {
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings, CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback); (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 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) { // Return early if ping passthrough is not enabled
continue; if (!mode.enabled()) {
} return CompletableFuture.completedFuture(fallback);
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);
} }
return pingResponses.thenApply(responses -> {
// Find the first non-fallback
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
ServerPing.Version version;
if (mode.version()) {
version = response.getVersion();
} else {
version = fallback.getVersion();
}
ServerPing.Players players;
if (mode.players()) {
players = response.getPlayers().orElse(null);
} else {
players = fallback.getPlayers().orElse(null);
}
Component description;
if (mode.description()) {
if (response.getDescriptionComponent() != null) {
description = response.getDescriptionComponent();
} else {
description = Component.empty();
}
} else {
description = fallback.getDescriptionComponent();
}
Favicon favicon;
if (mode.favicon()) {
favicon = response.getFavicon().orElse(null);
} else {
favicon = fallback.getFavicon().orElse(null);
}
ModInfo modinfo;
if (mode.modinfo()) {
modinfo = response.getModinfo().orElse(null);
} else {
modinfo = fallback.getModinfo().orElse(null);
}
return new ServerPing(
version,
players,
description,
favicon,
modinfo
);
}
return fallback;
});
} }
/** /**
@@ -165,7 +177,7 @@ public class ServerListPingHandler {
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION; ? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
PingPassthroughMode passthroughMode = configuration.getPingPassthrough(); PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
if (passthroughMode == PingPassthroughMode.DISABLED) { if (!passthroughMode.enabled()) {
return CompletableFuture.completedFuture(constructLocalPing(shownVersion)); return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
} else { } else {
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString) String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
@@ -25,14 +25,13 @@ import com.velocitypowered.api.permission.Tristate;
import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.permission.PermissionChecker; 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.Pointers;
import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
@@ -59,11 +58,10 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
private final VelocityServer server; private final VelocityServer server;
private PermissionFunction permissionFunction = ALWAYS_TRUE; private PermissionFunction permissionFunction = ALWAYS_TRUE;
private final @NotNull Pointers pointers = ConsoleCommandSource.super.pointers().toBuilder() private static final @NotNull PointersSupplier<VelocityConsole> POINTERS = PointersSupplier.<VelocityConsole>builder()
.withDynamic(PermissionChecker.POINTER, this::getPermissionChecker) .resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
.withDynamic(Identity.LOCALE, () -> ClosestLocaleMatcher.INSTANCE .resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
.lookupClosest(Locale.getDefault())) .lookupClosest(Locale.getDefault()))
.withStatic(FacetPointers.TYPE, Type.CONSOLE)
.build(); .build();
public VelocityConsole(VelocityServer server) { public VelocityConsole(VelocityServer server) {
@@ -71,8 +69,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
} }
@Override @Override
public void sendMessage(@NonNull Identity identity, @NonNull Component message, public void sendMessage(@NonNull Component message) {
@NonNull MessageType messageType) {
componentLogger.info(message); componentLogger.info(message);
} }
@@ -110,6 +107,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
protected LineReader buildReader(LineReaderBuilder builder) { protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder return super.buildReader(builder
.appName("Velocity") .appName("Velocity")
.variable(LineReader.HISTORY_FILE, Path.of(".console_history"))
.completer((reader, parsedLine, list) -> { .completer((reader, parsedLine, list) -> {
try { try {
List<String> offers = this.server.getCommandManager() List<String> offers = this.server.getCommandManager()
@@ -136,6 +134,10 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
if (!this.server.getCommandManager().executeAsync(this, command).join()) { if (!this.server.getCommandManager().executeAsync(this, command).join()) {
sendMessage(Component.translatable("velocity.command.command-does-not-exist", sendMessage(Component.translatable("velocity.command.command-does-not-exist",
NamedTextColor.RED)); NamedTextColor.RED));
return;
}
if (server.getConfiguration().isLogCommandExecutions()) {
logger.info("CONSOLE -> executed command /{}", command);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("An error occurred while running this command.", e); logger.error("An error occurred while running this command.", e);
@@ -149,6 +151,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
@Override @Override
public @NotNull Pointers pointers() { public @NotNull Pointers pointers() {
return pointers; return POINTERS.view(this);
} }
} }
@@ -60,9 +60,7 @@ final class CustomHandlerAdapter<F> {
UntargetedEventHandler buildUntargetedHandler(final Method method) UntargetedEventHandler buildUntargetedHandler(final Method method)
throws IllegalAccessException { throws IllegalAccessException {
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method); final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn( final LambdaType<F> lambdaType = functionType.defineClassesWith(methodHandlesLookup);
method.getDeclaringClass(), methodHandlesLookup);
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle); final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
final BiFunction<Object, Object, EventTask> handlerFunction = final BiFunction<Object, Object, EventTask> handlerFunction =
handlerBuilder.apply(invokeFunction); handlerBuilder.apply(invokeFunction);
@@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager {
} else { } else {
type = untargetedVoidHandlerType; type = untargetedVoidHandlerType;
} }
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle); return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle);
} }
static final class MethodHandlerInfo { static final class MethodHandlerInfo {
@@ -545,6 +545,36 @@ public class VelocityEventManager implements EventManager {
} }
} }
private <E> void fire(final @Nullable CompletableFuture<E> future, final E event,
final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
for (int i = offset; i < registrations.length; i++) {
final HandlerRegistration registration = registrations[i];
try {
final EventTask eventTask = registration.handler.executeAsync(event);
if (eventTask == null) {
continue;
}
final ContinuationTask<E> continuationTask = new ContinuationTask<>(eventTask,
registrations, future, event, i, currentlyAsync);
if (currentlyAsync || !eventTask.requiresAsync()) {
if (continuationTask.execute()) {
continue;
}
} else {
registration.plugin.getExecutorService().execute(continuationTask);
}
// fire will continue in another thread once the async task is
// executed and the continuation is resumed
return;
} catch (final Throwable t) {
logHandlerException(registration, t);
}
}
if (future != null) {
future.complete(event);
}
}
private static final int TASK_STATE_DEFAULT = 0; private static final int TASK_STATE_DEFAULT = 0;
private static final int TASK_STATE_EXECUTING = 1; private static final int TASK_STATE_EXECUTING = 1;
private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2; private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2;
@@ -669,36 +699,6 @@ public class VelocityEventManager implements EventManager {
} }
} }
private <E> void fire(final @Nullable CompletableFuture<E> future, final E event,
final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
for (int i = offset; i < registrations.length; i++) {
final HandlerRegistration registration = registrations[i];
try {
final EventTask eventTask = registration.handler.executeAsync(event);
if (eventTask == null) {
continue;
}
final ContinuationTask<E> continuationTask = new ContinuationTask<>(eventTask,
registrations, future, event, i, currentlyAsync);
if (currentlyAsync || !eventTask.requiresAsync()) {
if (continuationTask.execute()) {
continue;
}
} else {
registration.plugin.getExecutorService().execute(continuationTask);
}
// fire will continue in another thread once the async task is
// executed and the continuation is resumed
return;
} catch (final Throwable t) {
logHandlerException(registration, t);
}
}
if (future != null) {
future.complete(event);
}
}
private static void logHandlerException( private static void logHandlerException(
final HandlerRegistration registration, final Throwable t) { final HandlerRegistration registration, final Throwable t) {
final PluginDescription pluginDescription = registration.plugin.getDescription(); final PluginDescription pluginDescription = registration.plugin.getDescription();
@@ -26,8 +26,10 @@ import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; 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.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
@@ -72,6 +74,17 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
new HandshakeSessionHandler(connection, this.server)); new HandshakeSessionHandler(connection, this.server));
ch.pipeline().addLast(Connections.HANDLER, connection); 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()) { if (this.server.getConfiguration().isProxyProtocol()) {
ch.pipeline().addFirst(new HAProxyMessageDecoder()); ch.pipeline().addFirst(new HAProxyMessageDecoder());
} }
@@ -116,7 +116,7 @@ public enum TransportType {
return NIO; return NIO;
} }
if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) { if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) {
return IO_URING; 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;
}
}
@@ -32,17 +32,20 @@ import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups * An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
* in a separate thread, avoiding blocking the Netty threads for an extended period of time * on a small bounded pool of separate threads, avoiding blocking the Netty threads for an
* and without the downsides of Netty's native DNS resolver. * extended period of time and without the downsides of Netty's native DNS resolver.
*/ */
public final class SeparatePoolInetNameResolver extends InetNameResolver { public final class SeparatePoolInetNameResolver extends InetNameResolver {
private static final int MAX_RESOLVE_THREADS = 8;
private final ExecutorService resolveExecutor; private final ExecutorService resolveExecutor;
private final InetNameResolver delegate; private final InetNameResolver delegate;
private final Cache<String, List<InetAddress>> cache; private final Cache<String, List<InetAddress>> cache;
@@ -56,11 +59,15 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
*/ */
public SeparatePoolInetNameResolver(EventExecutor executor) { public SeparatePoolInetNameResolver(EventExecutor executor) {
super(executor); super(executor);
this.resolveExecutor = Executors.newSingleThreadExecutor( ThreadPoolExecutor resolveExecutor = new ThreadPoolExecutor(
MAX_RESOLVE_THREADS, MAX_RESOLVE_THREADS,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder() new ThreadFactoryBuilder()
.setNameFormat("Velocity DNS Resolver") .setNameFormat("Velocity DNS Resolver #%d")
.setDaemon(true) .setDaemon(true)
.build()); .build());
resolveExecutor.allowCoreThreadTimeOut(true);
this.resolveExecutor = resolveExecutor;
this.delegate = new DefaultNameResolver(executor); this.delegate = new DefaultNameResolver(executor);
this.cache = Caffeine.newBuilder() this.cache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS) .expireAfterWrite(30, TimeUnit.SECONDS)
@@ -71,7 +78,7 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception { protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
List<InetAddress> addresses = cache.getIfPresent(inetHost); List<InetAddress> addresses = cache.getIfPresent(inetHost);
if (addresses != null) { if (addresses != null) {
promise.trySuccess(addresses.get(0)); promise.trySuccess(addresses.getFirst());
return; return;
} }

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