Compare commits

...
49 Commits
Author SHA1 Message Date
Jones 1671ccac55 Release 2.0.4 (#66) 2023-10-18 16:38:22 +02:00
dependabot[bot] 8e03bdac5f build(deps): bump io.netty:netty-all from 4.1.99.Final to 4.1.100.Final (#64)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.99.Final to 4.1.100.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.99.Final...netty-4.1.100.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 16:35:22 +02:00
Jones 7c0bdc568e Continuation of the integration of 1.20.2 (#65)
* style(all): general code reformat

* feat: remove ActiveFeatures packet

* refactor: create packages for each state

* feat: fully recode JoinGame and RegistrySync packets

* feat: [wip] implement configuration state support to verification handler

* fix: encoder/decoder state not being set properly

* feat: integrate configuration state with the rest of the verification

* fix: PositionLook packet having an invalid packet id on 1.20.2

* fix: don't create and cache chunk mask for 1.17 clients

* style: remove comment; there's not really any BF code left

* feat: add todo for 1.20.2 barrier block mappings

* feat: exempt 1.20.2 clients from gravity and collision checks (TODO)
2023-10-18 16:34:37 +02:00
Jones 354c7a0143 Prepare 1.20.2 support and fix deserialization issue (#62)
* fix: compatibility with the latest version of Velocity (git-eb594fc7-b274)

* fix: implement invalid protocol disconnect if the version is unknown

* feat: add already-online disconnect message translation

* feat: implement 1.20.2 support for JoinGame packet

* feat: add new packets for 1.20.2 config state

* feat: [wip] add packet mappings for 1.20.2 (config state not included)

* fix: implement backwards compatibility for Velocity b265 and below

* fix: optimize login handling by directly running it in the channel's event loop

* feat: make lockdown bypass permission customizable

* fix(common): make all packets more debugging-friendly

* feat: [wip] implement config protocol state

* style: add comments for documenting FallbackPacketDecoder

* fix(all): deserialization not working for some components (https://github.com/jonesdevelopment/sonar/issues/63)
2023-10-14 15:12:39 +02:00
Jones bb33aa3fa4 Release 2.0.3 (#60) 2023-10-08 11:55:52 +02:00
Jones a0e308321d Overhaul all configurations and add more translations (#59)
* feat: create subclasses for different configuration value types

* fix: only send UpdateSectionBlocks packet if the collision check is enabled

* feat: restructure all configurations (config.yml, en.yml)

* fix: invalid configuration path for certain settings

* fix: use default comment stype because empty lines look ugly

* feat: update configuration yaml file header

* fix: smaller configuration fixes and relocations

* feat: migrate messages to modern MiniMessage formatting

* fix: verbose not deserializing the component properly

* feat: refactor commands into their own category in the config

* feat: remove unused postReload task/method

* fix: make hover component in /sonar actually look decent

* feat: make /sonar stats completely configurable

* fix: use MiniMessage newline tag instead of hardcoded newline symbol

* fix: use correct translation for /sonar stats cpu

* style: remove resolved todo comment

* refactor: lang/en.yml -> translations/en.yml
2023-10-07 16:33:29 +02:00
Jones 71b6d39eab Recode some commands and implement new dump system (#58)
* feat: reimplement the dumping system using Gson

* feat(subcommands): recoded blacklist/verified IP checking logic

* feat: add unknown IP address (Unknown Host) translation to config

* fix: start with a default queue size of 1 in the I/O lock for /sonar verified remove

* fix: lock IP addresses in /sonar verified history to avoid double I/O operations

* Revert "fix: lock IP addresses in /sonar verified history to avoid double I/O operations"

This reverts commit 6b092100befa423272f1eb310f5d1f8a0e9afb82.

* feat: recode /sonar stats to be more reliable and stable

* feat: update several subcommand descriptions to be more accurate

* fix: set lockdown bypass permission to sonar.lockdown.bypass
2023-10-07 10:44:51 +02:00
Jones 3faf8c8b7f General code cleanup and improvements (#57)
* style(api): small codestyle improvements and QoL updates

* style: reformat code using IntelliJ's code refactoring tool
2023-10-06 22:06:53 +02:00
Jones b45626eafb Implement update checker (#56)
* feat: implement update checker on server start

* feat: add non-main-branch message to update checker to avoid confusion
2023-10-06 21:46:54 +02:00
Jones 702ea1b370 Use barriers instead of stone blocks for verification (#54)
* feat: replace abstract method with a java.util.function.Function

* fix: make spawn x and z position adapt to platform size

* feat: optimize/improve Fallback packet preparing

* fix: block id shifting being broken on 1.7-1.13

* fix: optimize 1.7 support in UpdateSectionBlocks packet

* style: improve codestyle of the UpdateSectionBlocks packet

* fix: use normal binary io reader for mappings instead of unlimited reader

* feat: implement universal Chat packet and add LOGIN / GAME states for better packet registry control

* feat: [wip] barrier block integration (block ids for 1.13+ are still missing)

* feat: use buffered writing more often to potentially reduce performance impact

* feat: optimize chunk sending by only flushing the packets if necessary

* refactor: setAbilitiesAndTeleport -> sendAbilitiesAndTeleport

* style: fix fields being named incorrectly

* feat: cache 1.7 empty chunk filler bytes

* feat: use barrier blocks instead of stone blocks for verification
2023-10-06 20:27:48 +02:00
Jones a75c409e13 Recode gravity and collision checks (#53)
* feat: recode gravity verification system to avoid issues on 1.7/1.8

* feat: check for too many invalid x/z ticks (changes)

* feat: check for too many ignored Y ticks (changes)

* feat: allow all position changes to be logged to console

* fix: false positive with invalid movement ticks

* feat: improve verbose message in the gravity check
2023-10-05 20:52:04 +02:00
Jones afe9f3f9ea Implement support for delayed writing of packets (#50)
* feat: implement support for sending packets buffered/delayed

* feat: send EmptyChunkData and UpdateSectionBlocks delayed to potentially improve performance
2023-10-04 22:09:59 +02:00
Jones ba63e80fd0 Fix client brand decoding not working on 1.7 clients (#49) 2023-10-04 21:54:42 +02:00
Jones 5ff7e23fcb Apply hardcoded packet size limits to some packets (#48) 2023-10-04 21:43:26 +02:00
Jones cc194b0d62 Fix missing relocations potentially causing classes to clash (#47)
* fix: exclude snakeyaml from being implemented twice

* fix: relocate some packages to avoid classes being loaded twice
2023-10-04 20:48:55 +02:00
Jones 4e4423e2ce Release 2.0.2 (#45) 2023-10-04 14:58:36 +02:00
Jones ddb65f2f5a Bump dependencies (#44)
* build(deps): bump netty from 4.1.98.Final to 4.1.99.Final

* build(deps): bump gradle from 8.2 to 8.3
2023-10-04 14:53:47 +02:00
Jones 254faecc90 Fix verbose animation being unordered and modifiable (#43)
* fix: make sure the animations are stored in a List, not a Collection

* fix: make animations unmodifiable after reading them from the config
2023-10-04 14:41:17 +02:00
Jones 582024eba1 Improve configuration style and remove unused values (#42)
* feat: use semantic field names for the configuration values

* fix: remove unused messages from the configuration

* feat: use lombok getter for all configuration values
2023-10-04 14:37:29 +02:00
Jones e1fe738ffe Make verbose message handler customizable (#41)
* feat: allow custom verbose handler via the API

* fix: allow modifications of the next action bar message

* fix: don't allow modifications of internal methods
2023-10-04 14:00:16 +02:00
Jones 5f3a439113 Fix permission message not showing on Velocity (#40)
* feat: move permission check to the command execution (https://github.com/jonesdevelopment/sonar/issues/39)

* fix: do not allow tab completion if the player does not have the required permission

* fix: handle https://github.com/jonesdevelopment/sonar-legacy/issues/7 using an environment variable
2023-10-04 12:47:59 +02:00
Jones 27cca23412 Fix lockdown message not being serialized properly (#37) 2023-10-02 19:18:07 +02:00
Jones 2d2c576ad4 Implement MiniMessage support (#35)
* fix(bungee): use correct main class for the bungee plugin

* fix(bungee, bukkit): don't reload the plugin on enable

* feat: implement MiniMessage (adventure) support

* feat(all): recode ServerWrapper

* refactor: make SonarCommand platform independent

* Implement automatic Velocity version increment (#32)

* feat: add MiniMessage support to the Disconnect packet

* fix: only use the adventure-platform dependency in the respective modules

* feat: add velocity-plugin.json that automatically adapts the version and plugin description

* fix: only save the configuration when necessary (https://github.com/jonesdevelopment/sonar/issues/33)

* feat: implement MiniMessage legacy deserialization and modern color formatting support

* fix(velocity): use correct sendMessage() function
2023-10-02 18:13:55 +02:00
Michel Elkenwaat 0adf7b0b6a Revert "Bump netty from 4.1.98.Final to 4.1.99.Final (#31)"
This reverts commit c6ff9370c2.
2023-10-02 18:09:29 +02:00
dependabot[bot] c6ff9370c2 Bump netty from 4.1.98.Final to 4.1.99.Final (#31)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.98.Final to 4.1.99.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.98.Final...netty-4.1.99.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 17:27:14 +02:00
Jones e33aac29c5 Release version 2.0.1 (#29) 2023-09-30 09:15:05 +02:00
Jones 42dc3af6c9 Implement unofficial branch warnings and improve version management (#28)
* feat: check for non-main-branch builds and mark them as experimental

* feat: implement Implementation-Vendor

* feat: store version information in gradle.properties

* fix(bungee, bukkit): only use the semantic version for the plugin.yml/bungee.yml
2023-09-27 16:51:12 +02:00
Jones ff402d5096 Fix configuration reset after InvalidConfigurationException (#27)
* fix: only create a new messages configuration object if the preferred language changed (https://github.com/jonesdevelopment/sonar/issues/26)

* feat(api): better exception handling when loading the configuration

* fix: only load the configuration if the file already exists (https://github.com/jonesdevelopment/sonar/issues/26)

* feat: create a backup of the config if something goes wrong (https://github.com/jonesdevelopment/sonar/issues/26)

* fix: config header not showing and options not applying
2023-09-27 16:13:12 +02:00
Jones 06a72bf3bd Implement simple event system (#25)
* refactor(api): use toString() instead of formattedDelay() for SystemTimer

* fix(api): actually use the elapsed() function in SystemTimer

* feat(api): implement simple event system

* refactor(api): FallbackPlayer -> FallbackUser
2023-09-25 23:40:34 +02:00
Jones 3fb4291163 Merge pull request #24 from jonesdevelopment/create-code-of-conduct
Create CODE_OF_CONDUCT.md
2023-09-25 22:03:08 +02:00
Jones cc57b1d978 Create CODE_OF_CONDUCT.md 2023-09-25 22:02:41 +02:00
Jones 750b42b5ab Merge pull request #23 from jonesdevelopment/dependabot/gradle/org.projectlombok-lombok-1.18.30
build(deps): bump org.projectlombok:lombok from 1.18.28 to 1.18.30
2023-09-25 21:54:40 +02:00
Jones ad017024ae Merge pull request #22 from jonesdevelopment/dependabot/gradle/io.netty-netty-all-4.1.98.Final
build(deps): bump io.netty:netty-all from 4.1.97.Final to 4.1.98.Final
2023-09-25 21:54:19 +02:00
dependabot[bot] 5887d82c19 build(deps): bump org.projectlombok:lombok from 1.18.28 to 1.18.30
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.28 to 1.18.30.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.28...v1.18.30)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 01:06:35 +00:00
dependabot[bot] b1e5d01062 build(deps): bump io.netty:netty-all from 4.1.97.Final to 4.1.98.Final
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.97.Final to 4.1.98.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.97.Final...netty-4.1.98.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 01:06:26 +00:00
Jones a50bf45176 Merge pull request #21 from jonesdevelopment/fix/1.7-support
Implement Minecraft 1.7 support
2023-09-23 11:07:39 +02:00
Michel Elkenwaat e0b5ad2a33 feat(fallback): implement UpdateSectionBlocks packet for 1.7 (https://github.com/jonesdevelopment/sonar/issues/20) 2023-09-23 11:01:32 +02:00
Michel Elkenwaat e5ee94362c fix(fallback): false positive with new fell too fast check 2023-09-23 10:54:01 +02:00
Michel Elkenwaat d877568b38 fix(fallback): 1.7 support for PositionLook packet 2023-09-23 10:53:27 +02:00
Michel Elkenwaat 0edec77135 fix(fallback): recode PositionLook packet (encode) 2023-09-23 01:03:01 +02:00
Michel Elkenwaat 54445cdfdb fix(fallback): fix EmptyChunkData packet being broken 2023-09-23 00:56:23 +02:00
Michel Elkenwaat 8545d60791 fix(fallback): gravity check being broken 2023-09-23 00:52:02 +02:00
Michel Elkenwaat 11327e2890 fix(fallback): 1.7-1.8.9 potentially unloaded chunks causing issues 2023-09-23 00:21:00 +02:00
Michel Elkenwaat ad511836f3 fix: exclude changedBlocks from toString() in UpdateSectionBlocks packet 2023-09-23 00:13:57 +02:00
Michel Elkenwaat 5cb79d3acf fix: implement 1.7 support to DefaultSpawnPosition packet 2023-09-23 00:08:55 +02:00
Michel Elkenwaat 032dd4f94e feat(fallback): implement packet limits to Position/PositionLook 2023-09-23 00:03:06 +02:00
Michel Elkenwaat e831c1a646 fix: remove leftover debug messages 2023-09-23 00:00:17 +02:00
Michel Elkenwaat f50c90a690 fix: KeepAlive state check now ignores 1.7-1.8.9 2023-09-22 23:57:45 +02:00
Michel Elkenwaat 1e7e4ba5f1 fix(fallback): Position and PositionLook packet not decoding correctly 2023-09-22 23:57:18 +02:00
90 changed files with 3462 additions and 2498 deletions

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@jonesdev.xyz.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+26 -12
View File
@@ -12,12 +12,9 @@ plugins {
apply(plugin = "io.toolebox.git-versioner")
rootProject.group = "xyz.jonesdev.sonar"
val semanticVersion = "2.0.0"
versioner {
pattern {
pattern = "$semanticVersion-%h(-%c)"
pattern = "$version-%h-%c-%b"
}
}
@@ -35,17 +32,24 @@ subprojects {
apply(plugin = "com.github.johnrengelman.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.28")
annotationProcessor("org.projectlombok:lombok:1.18.28")
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
testCompileOnly("org.projectlombok:lombok:1.18.28")
testAnnotationProcessor("org.projectlombok:lombok:1.18.28")
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
compileOnly("io.netty:netty-all:4.1.97.Final") // netty
val adventureVersion = "4.14.0"
// adventure
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
// adventure nbt
implementation("net.kyori:adventure-nbt:$adventureVersion")
implementation("net.kyori:adventure-nbt:4.14.0") // nbt
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
}
}
@@ -62,14 +66,24 @@ tasks {
// Set the implementation version, so we can create exact version
// information in-game and make it accessible to the user.
attributes["Implementation-Version"] = version
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
}
}
shadowJar {
// bStats has to be relocated to the Sonar package otherwise it throws an exception.
relocate("org.bstats", "xyz.jonesdev.sonar.bstats")
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
// bStats has to be relocated to the Sonar package otherwise it throws an exception
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
// https://github.com/jonesdevelopment/sonar/issues/46
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.velocitypowered.natives", "xyz.jonesdev.sonar.libs.natives")
}
compileJava {
+2
View File
@@ -0,0 +1,2 @@
group=xyz.jonesdev.sonar
version=2.0.4
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+3 -1
View File
@@ -1,5 +1,7 @@
dependencies {
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") // yaml config
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
exclude(group = "org.yaml")
}
compileOnly("org.jetbrains:annotations:24.0.1")
}
@@ -21,6 +21,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
@@ -53,6 +54,12 @@ public interface Sonar {
@NotNull Verbose getVerboseHandler();
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
* Reloads the entire plugin
*/
@@ -63,10 +70,15 @@ public interface Sonar {
return SonarVersion.GET;
}
@NotNull
default SonarEventManager getEventManager() {
// We don't want anyone else to create a new instance.
return SonarEventManager.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We only have one single instance of Fallback,
// and we don't want anyone else to create a new instance.
// We don't want anyone else to create a new instance.
return Fallback.INSTANCE;
}
@@ -24,6 +24,7 @@ public final class SonarVersion {
static final SonarVersion GET = new SonarVersion();
private final String semanticVersion, full, formatted, commitSHA;
private final int build;
private final boolean isOnMainBranch;
SonarVersion() {
final Package manifest = Sonar.class.getPackage();
@@ -31,11 +32,12 @@ public final class SonarVersion {
if (versionString == null) {
this.full = "<could not retrieve version>";
this.semanticVersion = "<unknown>";
this.commitSHA = "<unknown>";
this.build = 0;
this.semanticVersion = "<unknown version>";
this.commitSHA = "<unknown commit>";
this.build = -1;
this.isOnMainBranch = false;
this.formatted = full;
Sonar.get().getLogger().error("Could not find version information (Is the manifest missing?)");
Sonar.get().getLogger().warn("Could not find version information. Is the manifest missing?");
return;
}
@@ -43,6 +45,7 @@ public final class SonarVersion {
this.semanticVersion = versionString.split("-")[0];
this.commitSHA = versionString.split("-")[1];
this.build = Integer.parseInt(versionString.split("-")[2]);
this.isOnMainBranch = versionString.split("-")[3].equals("main");
this.formatted = semanticVersion + " build " + build + " (" + commitSHA + ")";
}
@@ -24,7 +24,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@Getter
@RequiredArgsConstructor
public final class CommandInvocation {
private final InvocationSender sender;
private final InvocationSource sender;
private final Subcommand command;
private final String[] rawArguments;
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final Audience audience;
/**
* Sends an empty chat message to the command executor
*/
public final void sendMessage() {
sendMessage(Component.empty());
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final Component component) {
audience.sendMessage(component);
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import java.util.*;
public interface SonarCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
List<Component> CACHED_HELP_MESSAGE = new Vector<>();
default void cacheHelpMessage() {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
Component.text("Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ ".", NamedTextColor.YELLOW),
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
Component.empty(),
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
Component.textOfChildren(
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
Component component = Component.textOfChildren(
Component.text(" ▪ ", NamedTextColor.GRAY),
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
Component.text(" - ", NamedTextColor.GRAY),
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
);
Component hoverComponent = Component.textOfChildren(
Component.text("Only players: ", NamedTextColor.GRAY),
Component.text(sub.getInfo().onlyPlayers() ? "✔" : "✗",
sub.getInfo().onlyPlayers() ? NamedTextColor.GREEN : NamedTextColor.RED),
Component.newline(),
Component.text("Require console: ", NamedTextColor.GRAY),
Component.text(sub.getInfo().onlyConsole() ? "✔" : "✗",
sub.getInfo().onlyConsole() ? NamedTextColor.GREEN : NamedTextColor.RED),
Component.newline(),
Component.text("Permission: ", NamedTextColor.GRAY),
Component.text(sub.getPermission(), NamedTextColor.WHITE)
);
if (sub.getInfo().aliases().length > 0) {
hoverComponent = hoverComponent
.append(Component.newline())
.append(Component.text("Aliases: ", NamedTextColor.GRAY))
.append(Component.text(sub.getAliases(), NamedTextColor.WHITE));
}
component = component
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " "))
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, hoverComponent));
CACHED_HELP_MESSAGE.add(component);
});
}
}
@@ -19,11 +19,14 @@ package xyz.jonesdev.sonar.api.command.subcommand;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -49,11 +52,30 @@ public abstract class Subcommand {
.collect(Collectors.joining(", "));
}
protected final void incorrectUsage(final @NotNull InvocationSender sender) {
sender.sendMessage(
SONAR.getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", info.name() + " (" + arguments + ")")
);
protected static @Nullable InetAddress checkIP(final InvocationSource source, final String rawIP) {
if (!IP_PATTERN.matcher(rawIP).matches()) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
return null;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
return null;
}
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
return null;
}
return inetAddress;
}
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", info.name() + " (" + arguments + ")"));
}
public abstract void execute(final @NotNull CommandInvocation invocation);
@@ -27,15 +27,19 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
@Getter
public final class SimpleYamlConfig {
private final File file;
private final YamlFile yaml;
private static final List<String> HEADER = Arrays.asList(
"",
"Sonar anti-bot version " + Sonar.get().getVersion(),
" Currently running on: " + Sonar.get().getServer().getPlatform().getDisplayName(),
" Need help? https://jonesdev.xyz/discord",
"Running Sonar version " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName(),
"Need help or have questions? https://jonesdev.xyz/discord",
"https://github.com/jonesdevelopment/sonar",
""
);
@@ -48,6 +52,7 @@ public final class SimpleYamlConfig {
throw new IllegalStateException("Could not create folder?!");
}
this.file = file;
this.yaml = new YamlFile(file.getPath());
}
@@ -59,16 +64,18 @@ public final class SimpleYamlConfig {
}
}
public void load() {
try {
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
public void load() throws Exception {
// https://github.com/jonesdevelopment/sonar/issues/26
// Only load the configuration if the file already exists
if (yaml.exists()) {
yaml.loadWithComments();
} else {
yaml.createOrLoadWithComments();
yaml.options().headerFormatter().commentPrefix("# ");
yaml.setHeader(String.join(Sonar.LINE_SEPARATOR, HEADER));
} catch (IOException exception) {
exception.printStackTrace(System.err);
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
}
public void set(final String path, final Object v) {
File diff suppressed because it is too large. Load diff
@@ -45,16 +45,14 @@ public final class VerifiedPlayerController {
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@Getter
private final @NotNull SonarConfiguration.DatabaseType cachedDatabaseType;
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
final SonarConfiguration config = Sonar.get().getConfig();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = config.DATABASE_TYPE;
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
return;
}
@@ -89,7 +87,7 @@ public final class VerifiedPlayerController {
_remove(inetAddress);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
@@ -131,7 +129,7 @@ public final class VerifiedPlayerController {
_add(player);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
@@ -181,7 +179,7 @@ public final class VerifiedPlayerController {
MAP.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
dao.deleteBuilder().delete();
}
} catch (SQLException exception) {
@@ -28,7 +28,8 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
@@ -77,13 +78,12 @@ public enum Dependency {
try (final InputStream inputStream = mvnRepoURL.openStream()) {
Files.createDirectories(tempFilePath.getParent());
Files.copy(inputStream, Files.createFile(tempFilePath), StandardCopyOption.REPLACE_EXISTING);
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
} catch (IOException exception) {
throw new IllegalStateException(exception);
}
Sonar.get().getLogger().info("Finished downloading {} ({}s)!",
tempFilePath.getFileName(), timer.formattedDelay());
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
}
return tempFilePath.toUri().toURL();
@@ -38,8 +38,8 @@ public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration config = Sonar.get().getConfig();
final URL[] urls = new URL[config.DATABASE_TYPE.getDependencies().length];
for (final Dependency dependency : config.DATABASE_TYPE.getDependencies()) {
final URL[] urls = new URL[config.getDatabase().getType().getDependencies().length];
for (final Dependency dependency : config.getDatabase().getType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
@@ -50,12 +50,12 @@ public class DependencyLoader {
urls[dependency.ordinal()] = url;
}
final String type = config.DATABASE_TYPE.name().toLowerCase();
final String databaseURL =
"jdbc:" + type + "://" + config.SQL_URL + ":" + config.SQL_PORT + "/" + config.SQL_DATABASE;
final String type = config.getDatabase().getType().name().toLowerCase();
final String databaseURL = String.format("jdbc:%s://%s:%d/%s",
type, config.getDatabase().getUrl(), config.getDatabase().getPort(), config.getDatabase().getName());
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config);
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
return new JdbcSingleConnectionSource(databaseURL, connection);
}
@@ -63,18 +63,16 @@ public class DependencyLoader {
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration configuration) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(configuration.DATABASE_TYPE.getDriverClassName());
final @NotNull SonarConfiguration.Database config) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
if (!configuration.SQL_USER.isEmpty()) {
properties.put("user", configuration.SQL_USER);
}
if (!configuration.SQL_PASSWORD.isEmpty()) {
properties.put("password", configuration.SQL_PASSWORD);
properties.put("user", config.getUsername());
if (!config.getPassword().isEmpty()) {
properties.put("password", config.getPassword());
}
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event;
public interface SonarEvent {
}
@@ -15,26 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.api.event;
public interface InvocationSender {
@FunctionalInterface
public interface SonarEventListener {
/**
* @return The name of the command executor
*/
String getName();
/**
* Sends a message to the command executor
* Handles the publishing of an event
*
* @param message Deserialized message
* @param event [Cancellable] Sonar event
*/
void sendMessage(final String message);
/**
* Sends an empty chat message to the command executor
*/
default void sendMessage() {
sendMessage("");
}
void handle(final SonarEvent event);
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.Vector;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SonarEventManager {
public static final SonarEventManager INSTANCE = new SonarEventManager();
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
/**
* Publishes the given event to all listeners
*/
@ApiStatus.Internal
public void publish(final @NotNull SonarEvent event) {
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
eventListener.handle(event);
}
}
/**
* Registers one (or more) event listeners
*
* @param listeners One (or more) listeners to register
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
*/
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
}
/**
* Unregisters one (or more) event listeners
*
* @param listeners One (or more) listeners to unregister
* @apiNote This does not have any effect if the given listeners are not registered
* @see #registerListener(SonarEventListener...) Register an event listener
*/
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.ApiStatus;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
private final FallbackUser<?, ?> user;
}
@@ -15,17 +15,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.api.event.impl;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.*;
public interface SonarBaseCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
List<Object> CACHED_HELP_MESSAGE = new Vector<>();
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser<?, ?> user;
private final String reason;
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
private final FallbackUser<?, ?> user;
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.UUID;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser<?, ?> user;
private final long timeTakenToVerify;
}
@@ -50,22 +50,22 @@ public final class Fallback {
@Override
public void info(final String message, final Object... args) {
SONAR.getLogger().info("[Fallback] " + message, args);
SONAR.getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
SONAR.getLogger().warn("[Fallback] " + message, args);
SONAR.getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
SONAR.getLogger().error("[Fallback] " + message, args);
SONAR.getLogger().error("[fallback] " + message, args);
}
};
public boolean isPotentiallyUnderAttack() {
return connected.size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK
|| queue.getQueuedPlayers().size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK;
final int min = SONAR.getConfig().getMinPlayersForAttack();
return connected.size() > min || queue.getQueuedPlayers().size() > min;
}
}
@@ -50,7 +50,7 @@ public final class FallbackQueue {
}
public void poll() {
final int max = Sonar.get().getConfig().MAXIMUM_QUEUE_POLLS;
final int max = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// We need to be very careful here since we don't want any concurrency issues.
@@ -19,18 +19,21 @@ package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
public interface FallbackPlayer<X, Y> {
public interface FallbackUser<X, Y> {
@NotNull Fallback getFallback();
@NotNull X getPlayer();
@@ -49,9 +52,20 @@ public interface FallbackPlayer<X, Y> {
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message
* @param reason Legacy disconnect message string
* @see #disconnect(Component)
*/
void disconnect(final @NotNull String reason);
default void disconnect(final @NotNull String reason) {
disconnect(Component.text(reason));
}
/**
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message component
*/
void disconnect(final @NotNull Component reason);
/**
* Sends a packet/message to the player
@@ -60,6 +74,12 @@ public interface FallbackPlayer<X, Y> {
*/
void write(final @NotNull Object msg);
/**
* Queues a buffered message that will be
* sent once all messages are flushed.
*/
void delayedWrite(final @NotNull Object msg);
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
/**
@@ -72,10 +92,13 @@ public interface FallbackPlayer<X, Y> {
*/
default void fail(final @Nullable String reason) {
if (getChannel().isActive()) {
disconnect(Sonar.get().getConfig().VERIFICATION_FAILED);
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_FAILED_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
@@ -89,8 +112,11 @@ public interface FallbackPlayer<X, Y> {
// Check if the player has too many failed attempts
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress().toString());
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_BLACKLIST_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.EnumSet;
@@ -66,10 +67,10 @@ public enum ProtocolVersion {
MINECRAFT_1_19_3(761),
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(-1);
MINECRAFT_1_20_2(764);
@Getter
private final int protocol;
private final int snapshotProtocol = -1;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
@@ -81,7 +82,6 @@ public enum ProtocolVersion {
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
versions.put(version.snapshotProtocol, version);
}
ID_TO_PROTOCOL_CONSTANT = versions;
@@ -104,10 +104,6 @@ public enum ProtocolVersion {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public int getProtocol() {
return protocol == -1 ? snapshotProtocol : protocol;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
@@ -17,22 +17,26 @@
package xyz.jonesdev.sonar.api.server;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import java.util.Optional;
public interface ServerWrapper {
@Getter
@RequiredArgsConstructor
public abstract class ServerWrapper {
/**
* @return Platform of the server (Velocity, BungeeCord, Spigot)
* Platform of the server (Velocity, BungeeCord, Spigot)
*/
SonarPlatform getPlatform();
private final SonarPlatform platform;
/**
* @param username Username of the player
* @return Optional player wrapped as InvocationSender
* @see xyz.jonesdev.sonar.api.command.InvocationSender
* @see InvocationSource
*/
Optional<InvocationSender> getOnlinePlayer(final String username);
public abstract Optional<InvocationSource> getOnlinePlayer(final String username);
}
@@ -35,7 +35,8 @@ public final class SystemTimer {
return delay() >= amount;
}
public String formattedDelay() {
@Override
public String toString() {
return String.format("%.3f", delay() / 1000D);
}
}
@@ -19,6 +19,8 @@ package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
@@ -47,7 +49,7 @@ public abstract class Verbose implements JVMProfiler {
totalJoins = Statistics.TOTAL_TRAFFIC.get();
// Statistically determine the joins per second without any caches
if (totalJoins > 0 && secondTimer.delay() >= 1000L) {
if (totalJoins > 0 && secondTimer.elapsed(1000L)) {
secondTimer.reset();
joinsPerSecond = totalJoins - lastTotalJoins;
lastTotalJoins = totalJoins;
@@ -57,8 +59,8 @@ public abstract class Verbose implements JVMProfiler {
broadcast(prepareActionBarFormat());
}
protected final @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().ACTION_BAR_LAYOUT
protected @NotNull Component prepareActionBarFormat() {
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
@@ -78,37 +80,37 @@ public abstract class Verbose implements JVMProfiler {
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
.replace("%max-memory%", formatMemory(getMaxMemory()))
.replace("%animation%", nextAnimation());
.replace("%animation%", nextAnimation()));
}
protected final String nextAnimation() {
val animations = Sonar.get().getConfig().ANIMATION;
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
// Run action bar verbose
protected abstract void broadcast(final String message);
protected abstract void broadcast(final Component component);
/**
* @param subscriber Name of the player who subscribed
* @return Whether the player is subscribed or not
*/
public boolean isSubscribed(final @NotNull String subscriber) {
public final boolean isSubscribed(final @NotNull String subscriber) {
return subscribers.contains(subscriber);
}
/**
* @param subscriber Name of the player to subscribe
*/
public void subscribe(final @NotNull String subscriber) {
public final void subscribe(final @NotNull String subscriber) {
subscribers.add(subscriber);
}
/**
* @param subscriber Name of the player to unsubscribe
*/
public void unsubscribe(final @NotNull String subscriber) {
public final void unsubscribe(final @NotNull String subscriber) {
subscribers.remove(subscriber);
}
}
+5 -2
View File
@@ -8,7 +8,7 @@ apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = rootProject.name
version = rootProject.version.toString()
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
@@ -17,7 +17,7 @@ bukkit {
commands {
register("sonar") {
permission = "sonar.command"
// No permission check here
}
}
}
@@ -31,6 +31,9 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
@@ -21,11 +21,12 @@ import lombok.Getter;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bukkit.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.command.BukkitInvocationSource;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
@@ -72,44 +73,25 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUKKIT) {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.VELOCITY;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getOnlinePlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getName();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(message);
}
});
.map(BukkitInvocationSource::new);
}
};
@Override
public void enable() {
// Reload configuration
reload();
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new SonarCommand());
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register Fallback queue task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
@@ -123,9 +105,4 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
2L, 2L);
}
@Override
public void postReload() {
// Do nothing
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.command;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class BukkitInvocationSource extends InvocationSource {
private static final BukkitAudiences AUDIENCES = BukkitAudiences.create(SonarBukkit.INSTANCE.getPlugin());
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
}
}
@@ -17,18 +17,14 @@
package xyz.jonesdev.sonar.bukkit.command;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.text.Component;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
@@ -39,62 +35,9 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBaseCommand {
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
".com/jonesdevelopment/sonar");
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
"Discord ");
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
"GitHub.");
static {
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
".com/jonesdevelopment/sonar"));
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"Discord)").create()));
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"));
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"GitHub)").create()));
}
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ "."),
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
GITHUB_LINK_COMPONENT,
new TextComponent(""),
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
new TextComponent(
DISCORD_SUPPORT,
GITHUB_ISSUES
),
new TextComponent("")
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
final TextComponent component = new TextComponent(
new TextComponent(ChatColor.GRAY + " ▪ "),
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
new TextComponent(ChatColor.GRAY + " - "),
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
);
component.setClickEvent(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
);
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
).create())
);
CACHED_HELP_MESSAGE.add(component);
});
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
{
cacheHelpMessage();
}
@Override
@@ -102,7 +45,15 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
final Command command,
final String label,
final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
@@ -112,16 +63,14 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(Sonar.get().getConfig().COMMAND_COOL_DOWN);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return false;
}
@@ -130,19 +79,6 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return sender.getName();
}
@Override
public void sendMessage(final String message) {
sender.sendMessage(message);
}
};
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -156,10 +92,8 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
.replace("%permission%", subcommand.get().getPermission())
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
@@ -167,24 +101,22 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -197,12 +129,8 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
if (sender instanceof Player) {
((Player) sender).spigot().sendMessage((TextComponent) component);
} else {
sender.sendMessage(((TextComponent) component).getText());
}
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
return false;
@@ -213,6 +141,10 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
final Command command,
final String commandAlias,
final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
@@ -18,7 +18,7 @@
package xyz.jonesdev.sonar.bukkit.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.text.Component;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@@ -27,14 +27,13 @@ import xyz.jonesdev.sonar.api.verbose.Verbose;
public final class ActionBarVerbose extends Verbose {
private final Server server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
@Override
public void broadcast(final Component component) {
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final Player player = server.getPlayer(subscriber);
if (player != null) {
//player.sendMessage(ChatMessageType.ACTION_BAR, component);
// TODO: use an adventure audience
}
}
}
+5 -2
View File
@@ -6,8 +6,8 @@ apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
version = rootProject.version.toString()
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
}
@@ -24,6 +24,9 @@ dependencies {
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
}
@@ -18,15 +18,15 @@
package xyz.jonesdev.sonar.bungee;
import lombok.Getter;
import net.md_5.bungee.api.chat.TextComponent;
import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bungee.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.command.BungeeInvocationSource;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
@@ -76,44 +76,25 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUNGEE) {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.BUNGEE;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getPlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getName();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(new TextComponent(message));
}
});
.map(BungeeInvocationSource::new);
}
};
@Override
public void enable() {
// Reload configuration
reload();
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new SonarCommand());
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
@@ -133,11 +114,4 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
}
@Override
public void postReload() {
// Prepare cached messages
FallbackListener.CachedMessages.update();
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class BungeeInvocationSource extends InvocationSource {
private static final BungeeAudiences AUDIENCES = BungeeAudiences.create(SonarBungee.INSTANCE.getPlugin());
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
}
}
@@ -17,12 +17,8 @@
package xyz.jonesdev.sonar.bungee.command;
import net.md_5.bungee.api.ChatColor;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
@@ -30,8 +26,8 @@ import net.md_5.bungee.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
@@ -42,72 +38,24 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand extends Command implements TabExecutor, SonarBaseCommand {
public SonarCommand() {
super("sonar", "sonar.command");
}
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
".com/jonesdevelopment/sonar");
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
"Discord ");
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
"GitHub.");
static {
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
".com/jonesdevelopment/sonar"));
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"Discord)").create()));
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"));
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"GitHub)").create()));
}
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ "."),
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
GITHUB_LINK_COMPONENT,
new TextComponent(""),
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
new TextComponent(
DISCORD_SUPPORT,
GITHUB_ISSUES
),
new TextComponent("")
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
final TextComponent component = new TextComponent(
new TextComponent(ChatColor.GRAY + " ▪ "),
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
new TextComponent(ChatColor.GRAY + " - "),
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
);
component.setClickEvent(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
);
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
).create())
);
CACHED_HELP_MESSAGE.add(component);
});
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
super("sonar");
cacheHelpMessage();
}
@Override
@SuppressWarnings({"redundantSuppression"})
public void execute(final @NotNull CommandSender sender, final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
@@ -117,16 +65,14 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().COMMAND_COOL_DOWN));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(new TextComponent(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
@@ -135,19 +81,6 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return sender.getName();
}
@Override
public void sendMessage(final String message) {
sender.sendMessage(new TextComponent(message));
}
};
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -161,10 +94,8 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
.replace("%permission%", subcommand.get().getPermission())
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
@@ -172,24 +103,22 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -202,14 +131,18 @@ public final class SonarCommand extends Command implements TabExecutor, SonarBas
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
sender.sendMessage((TextComponent) component);
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
}
@Override
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
@@ -21,8 +21,11 @@ import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.util.Natives;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.compress.PacketCompressor;
@@ -30,17 +33,18 @@ import net.md_5.bungee.compress.PacketDecompressor;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
import static xyz.jonesdev.sonar.bungee.fallback.FallbackListener.CachedMessages.LOCKDOWN_DISCONNECT;
@RequiredArgsConstructor
public final class FallbackListener implements Listener {
@@ -57,36 +61,35 @@ public final class FallbackListener implements Listener {
}
}
public static class CachedMessages {
static TextComponent LOCKDOWN_DISCONNECT;
public static void update() {
LOCKDOWN_DISCONNECT = new TextComponent(Sonar.get().getConfig().LOCKDOWN_DISCONNECT);
}
}
@EventHandler
@SuppressWarnings("deprecation")
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
event.getPlayer().disconnect(LOCKDOWN_DISCONNECT);
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
}
if (Sonar.get().getConfig().LOCKDOWN_LOG_ATTEMPTS) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().LOCKDOWN_CONSOLE_LOG
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion()))
);
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
}
return;
} else if (Sonar.get().getConfig().LOCKDOWN_ENABLE_NOTIFY) {
event.getPlayer().sendMessage(
new TextComponent(Sonar.get().getConfig().LOCKDOWN_NOTIFICATION)
);
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdown().getNotification()));
}
}
@@ -22,20 +22,20 @@ import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@Getter
@RequiredArgsConstructor
public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrapper, FallbackInitialHandler> {
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
private final Fallback fallback;
private final ChannelWrapper player;
private final FallbackInitialHandler connection;
@@ -45,8 +45,8 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrappe
private final ProtocolVersion protocolVersion;
@Override
public void disconnect(final @NotNull String reason) {
connection.closeWith(Disconnect.create(TextComponent.toLegacyText(new TextComponent(reason))));
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
@@ -57,4 +57,13 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrappe
ReferenceCountUtil.release(packet);
}
}
@Override
public void delayedWrite(final @NotNull Object packet) {
if (channel.isActive()) {
channel.write(packet, channel.voidPromise());
} else {
ReferenceCountUtil.release(packet);
}
}
}
@@ -29,7 +29,7 @@ import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.FallbackPlayerWrapper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackUserWrapper;
import java.net.InetAddress;
import java.util.Objects;
@@ -43,7 +43,7 @@ public final class FallbackInitialHandler extends InitialHandler {
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
private ChannelWrapper channelWrapper;
@SuppressWarnings("unused") // TODO: remove later
private @Nullable FallbackPlayerWrapper player;
private @Nullable FallbackUserWrapper player;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
@@ -58,7 +58,7 @@ public final class FallbackInitialHandler extends InitialHandler {
if (player == null && !Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
final Channel channel = channelWrapper.getHandle();
player = new FallbackPlayerWrapper(
player = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress,
ProtocolVersion.fromId(getHandshake().getProtocolVersion())
@@ -27,13 +27,10 @@ import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
@@ -42,8 +39,7 @@ import static net.md_5.bungee.netty.PipelineUtils.*;
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
// cached object of our configuration to instantly access it
private static final SonarConfiguration CONFIG = Objects.requireNonNull(Sonar.get().getConfig());
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
private static final WriteBufferWaterMark MARK;
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
@@ -75,7 +71,7 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(MARK);
channel.pipeline().addLast(FRAME_DECODER, CONFIG.REPLACE_VAR_INT_DECODER
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS
@@ -18,9 +18,8 @@
package xyz.jonesdev.sonar.bungee.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatMessageType;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@@ -28,14 +27,13 @@ import xyz.jonesdev.sonar.api.verbose.Verbose;
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
@Override
public void broadcast(final Component component) {
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final ProxiedPlayer player = server.getPlayer(subscriber);
if (player != null) {
player.sendMessage(ChatMessageType.ACTION_BAR, component);
// TODO: use an adventure audience
}
}
}
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.common.boot;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
@@ -32,6 +33,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import java.io.File;
import java.net.InetAddress;
@@ -41,6 +43,7 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
@@ -63,13 +66,20 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
public final void initialize() {
getLogger().info("Successfully booted in {}s!", launchTimer.formattedDelay());
// Check if the branch is not the main branch to warn about unstable versions
if (!getVersion().isOnMainBranch()) {
getLogger().warn("You are currently using an unofficial experimental branch.");
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
}
getLogger().info("Successfully booted in {}s!", launchTimer);
getLogger().info("Initializing shared components...");
// Reload configuration
reload();
getLogger().info("Successfully initialized components in {}s!", launchTimer.formattedDelay());
getLogger().info("Successfully initialized components in {}s!", launchTimer);
getLogger().info("Enabling all tasks and features...");
try {
@@ -77,11 +87,16 @@ public abstract class SonarBootstrap<T> implements Sonar {
enable();
// Done
getLogger().info("Done ({}s)!", launchTimer.formattedDelay());
getLogger().info("Done ({}s)!", launchTimer);
} catch (Throwable throwable) {
// An error has occurred
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
throwable.printStackTrace(System.err);
return; // Do not check for updates if the launch failed
}
// Check if a new version has been released
UpdateChecker.checkForUpdates();
}
public abstract void enable();
@@ -92,7 +107,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (getVerifiedPlayerController() != null
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().DATABASE_TYPE) {
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
}
@@ -102,19 +117,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Update ratelimiter
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
getConfig().VERIFICATION_DELAY, TimeUnit.MILLISECONDS, 250L
);
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
// Reinitialize database controller
verifiedPlayerController = new VerifiedPlayerController();
// Call post reload task
postReload();
}
public abstract void postReload();
public void shutdown() {
getLogger().info("Starting shutdown process...");
// ...
@@ -20,77 +20,111 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private final @NotNull FallbackPlayer<?, ?> player;
private final @NotNull FallbackUser<?, ?> user;
private final String username;
private final UUID uuid;
private final short transactionId;
private final long verifyKeepAliveId;
private int movementTick, receivedPackets;
private double lastX, lastY, lastZ;
private final UUID playerUuid;
private short expectedTransactionId;
private long expectedKeepAliveId;
private int expectedTeleportId = -1;
private int tick, totalReceivedPackets;
private int ignoredMovementTicks;
private double posX, posY, posZ, lastY;
private boolean resolvedClientBrand, resolvedClientSettings;
@Setter
private State state;
private @NotNull State state = State.LOGIN_ACK;
private boolean listenForMovements;
private final SystemTimer login = new SystemTimer();
private static final Random RANDOM = new Random();
@RequiredArgsConstructor
public enum State {
// LOGIN
KEEP_ALIVE(false),
// PLAY
CLIENT_SETTINGS(false),
PLUGIN_MESSAGE(false),
TRANSACTION(false),
// IN-GAME
TELEPORT(false),
POSITION(true),
COLLISIONS(true),
// OTHER
SUCCESS(false);
private final boolean canMove;
// 1.20.2 configuration state
LOGIN_ACK, CONFIGURE,
// pre-JOIN ping check
KEEP_ALIVE,
// post-JOIN checks
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
// PLAY checks
TELEPORT, POSITION, COLLISIONS,
// Done
SUCCESS
}
public FallbackVerificationHandler(final @NotNull FallbackPlayer<?, ?> player,
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
final @NotNull String username,
final @NotNull UUID uuid) {
this.player = player;
final @NotNull UUID playerUuid) {
this.user = user;
this.username = username;
this.uuid = uuid;
this.transactionId = (short) RANDOM.nextInt();
this.verifyKeepAliveId = RANDOM.nextInt();
this.state = State.KEEP_ALIVE;
this.playerUuid = playerUuid;
// Send LoginSuccess packet to make the client think they are joining the server
player.write(new ServerLoginSuccess(username, uuid));
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
initialJoinProcess();
}
}
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
private void configure() {
// Set the state to CONFIGURE to avoid false positives
state = State.CONFIGURE;
// Send necessary configuration packets to the client
user.delayedWrite(synchronizeRegistry);
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
if (decoder != null && encoder != null) {
// Update the packet registry state to be able to listen for CONFIG packets
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
}
private void initialJoinProcess() {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Set the state to KEEP_ALIVE to avoid false positives
state = State.KEEP_ALIVE;
// Generate a random KeepAlive ID for the pre-join check
expectedKeepAliveId = RANDOM.nextInt();
// Send first KeepAlive to check if the connection is somewhat responsive
player.write(new KeepAlive(verifyKeepAliveId));
user.write(new KeepAlive(expectedKeepAliveId));
}
}
@@ -98,18 +132,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Set the state to TRANSACTION to avoid false positives
// and go on with the flow of the verification.
state = State.TRANSACTION;
// Send a transaction with a
player.write(new Transaction(
0, transactionId, false
));
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new Transaction(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
}
// Select the JoinGame packet for the respective protocol version
player.write(getJoinPacketForVersion(player.getProtocolVersion()));
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
sendTransaction();
}
}
private void sendAbilitiesAndTeleport() {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Make sure the player is unable to fly (the player is in spectator mode)
user.delayedWrite(DEFAULT_ABILITIES);
// Generate the current teleport ID
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
// Teleport the player to the spawn position
user.delayedWrite(new PositionLook(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0f, 0f, expectedTeleportId, false
));
}
private void sendChunkData() {
@@ -117,38 +174,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// and go on with the flow of the verification.
state = State.POSITION;
// Teleport player into the fake lobby by sending an empty chunk
player.write(EMPTY_CHUNK_DATA);
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
player.write(UPDATE_SECTION_BLOCKS);
user.delayedWrite(EMPTY_CHUNK_DATA);
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(UPDATE_SECTION_BLOCKS);
}
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @NotNull FallbackPlayer<?, ?> player, final String locale) {
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().VALID_LOCALE_REGEX;
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
private static boolean validateClientBrand(final @NotNull FallbackPlayer<?, ?> player, final ByteBuf content) {
private static boolean validateClientBrand(final @NotNull FallbackUser<?, ?> user, final ByteBuf content) {
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
// No need to check for empty brands since ProtocolUtil#readBrandMessage
// already performs these checks by default.
final String read = ProtocolUtil.readBrandMessage(content);
// Check if the decoded client brand string is too long
if (read.length() > Sonar.get().getConfig().getVerification().getMaxBrandLength()) {
return false;
}
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().VALID_BRAND_REGEX;
// 1.7 has some very weird issues when trying to decode the client brand
final boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0;
final int cap = Sonar.get().getConfig().MAXIMUM_BRAND_LENGTH;
// Read the client brand using our custom readString method that supports 1.7.
// The legacy version of readString does not compare the string length
// with the VarInt sent by the client.
final String read = ProtocolUtil.readString(content, cap, legacy);
// No need to check for empty or too long client brands since
// ProtocolUtil#readString already does exactly that.
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
// Fail if the string (client brand) could not be decoded properly
player.fail("could not decode string");
user.fail("could not decode string");
// Throw the exception so we don't continue checking
throw exception;
}
@@ -156,26 +216,52 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified, drop all other packets
if (state == State.SUCCESS) return;
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().MAXIMUM_LOGIN_PACKETS
+ Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
checkFrame(++receivedPackets < maxPackets, "too many packets");
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().VERIFICATION_TIMEOUT;
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
// Check if the time limit has exceeded
if (login.delay() > timeout) {
player.getChannel().close();
if (login.elapsed(timeout)) {
user.getChannel().close();
return;
}
if (packet instanceof LoginAcknowledged) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(State.LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
}
if (packet instanceof FinishConfiguration) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(State.CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
}
if (packet instanceof KeepAlive) {
final KeepAlive keepAlive = (KeepAlive) packet;
// 1.7-1.8.9 are sending a KeepAlive packet with the ID 0 every 20 ticks
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
// Check if we are currently expecting a KeepAlive packet
assertState(State.KEEP_ALIVE);
final KeepAlive keepAlive = (KeepAlive) packet;
checkFrame(keepAlive.getId() == verifyKeepAliveId, "invalid KeepAlive ID");
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
// The correct KeepAlive packet has been received
sendJoinGamePacket();
@@ -185,35 +271,44 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
final ClientSettings clientSettings = (ClientSettings) packet;
// Validate the locale using a regex to filter unwanted characters.
checkFrame(validateClientLocale(player, clientSettings.getLocale()), "invalid locale");
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (state == State.CLIENT_SETTINGS) {
state = State.PLUGIN_MESSAGE;
}
// Make sure we mark the ClientSettings as valid
resolvedClientSettings = true;
}
if (packet instanceof PluginMessage) {
final PluginMessage pluginMessage = (PluginMessage) packet;
// Only the brand channel is important
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
|| pluginMessage.getChannel().equals("minecraft:brand")) {
// Check if the brand packet was sent twice,
// which is not possible when using a vanilla Minecraft client.
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
// the legacy namespace system ('MC|' + channel).
final boolean v1_13 = player.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
// Validate the client branding using a regex to filter unwanted characters.
checkFrame(validateClientBrand(player, pluginMessage.content()), "invalid client brand");
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (state == State.PLUGIN_MESSAGE) {
// Send the transaction packet
sendTransaction();
}
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
}
@@ -226,27 +321,33 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
// Check if the transaction ID is valid
checkFrame(transaction.getId() == transactionId, "invalid transaction id");
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().CHECK_GRAVITY) {
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
finish();
return;
}
// Make sure the player is unable to fly (the player is in spectator mode)
player.write(DEFAULT_ABILITIES);
// Teleport the player to the spawn position
player.write(SPAWN_TELEPORT);
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
finish();
return;
}
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
sendAbilitiesAndTeleport();
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleports
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
sendChunkData();
} else {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Send all previously sent packets in one flush since we didn't flush
// the channel earlier when we teleported the player.
user.getChannel().flush();
}
}
@@ -254,47 +355,53 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Check if we are currently expecting a TeleportConfirm packet
assertState(State.TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirm teleportConfirm = (TeleportConfirm) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Check if the teleport id is correct
final boolean teleportIdCorrect = teleportConfirm.getTeleportId() == SPAWN_TELEPORT.getTeleportId();
checkFrame(teleportIdCorrect, "invalid teleport ID");
// Reset all values to ensure safety on teleport
tick = 1;
posY = lastY = -1;
expectedTeleportId = -1;
// Now we can send the chunk data
sendChunkData();
}
// Only handle positions if the player can send position packets.
if (state.canMove) {
if (state != State.LOGIN_ACK) {
if (packet instanceof Position) {
final Position position = (Position) packet;
// Immediately handle new position update
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof PositionLook) {
final PositionLook position = (PositionLook) packet;
// Immediately handle new position update
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof Player) {
final Player player = (Player) packet;
// This packet does not send new position data, reuse the last Y
handlePositionUpdate(lastX, lastY, lastZ, player.isOnGround());
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
}
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
final double deltaY = lastY - y;
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
// Then, check if the position is equal to the spawn position
&& x == SPAWN_X_POSITION && y == dynamicSpawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = -1;
expectedTeleportId = -1;
}
lastX = x;
lastY = y;
lastZ = z;
posX = x;
lastY = posY;
posY = y;
posZ = z;
// The player is not allowed to move away from the collision platform.
// This should not happen unless the max movement tick is configured to a high number.
@@ -304,56 +411,85 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The onGround property can never be true when we aren't checking for collisions
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
// Skip teleports using this small check
if (deltaY > 0.07) {
// Check if the client hasn't moved before sending the first movement packet
if (!listenForMovements) {
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
// Now, once we verified the X and Z position, we can safely check for gravity
listenForMovements = true;
}
// Verify the player if they sent correct movement packets
if (movementTick++ >= MAX_MOVEMENT_TICK) {
if (Sonar.get().getConfig().CHECK_COLLISIONS) {
if (state != State.COLLISIONS) {
// Set the state to COLLISIONS to avoid false positives
// and go on with the flow of the verification.
// Now we don't care about gravity anymore,
// we just want the player to collide with the blocks.
state = State.COLLISIONS;
} else {
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
lastY = dynamicSpawnYPosition;
return;
}
// The offset cannot greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
final double deltaY = lastY - y;
// The deltaY is 0 whenever the player sends their first position packet.
// We have to account for this or the player will fail the verification.
if (deltaY == 0) {
// Check for too many ignored Y ticks
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks();
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
return;
}
// Check if the player is colliding by performing a basic Y offset check.
if (ground) {
// The player is colliding, finish verification
// TODO: Check for: velocity, entities/mounting, interactions
finish();
}
}
} else {
// Checking collisions is disabled, just finish verification
finish();
}
} else if (y >= DEFAULT_Y_COLLIDE_POSITION && y <= DYNAMIC_SPAWN_Y_POSITION) {
// This is a basic gravity check that predicts the next y position
final double predictedY = PREPARED_MOVEMENT_PACKETS[movementTick];
final double offsetY = Math.abs(deltaY - predictedY);
if (tick > maxMovementTick) {
if (!Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Checking collisions is disabled, just finish verification
finish();
return;
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.01, "invalid offset: " + y + ", " + offsetY);
if (state != State.COLLISIONS) {
// Now we don't care about gravity anymore,
// we just want the player to collide with the blocks.
state = State.COLLISIONS;
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - offset: {}", username, x, y, z, offsetY);
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
if (ground) {
// The player is colliding to blocks, finish verification
finish();
return;
}
}
if (!ground && tick < maxPredictionTick) {
final double predictedY = preparedCachedYMotions[tick];
final double offsetY = Math.abs(deltaY - predictedY);
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
tick, y, offsetY, deltaY, predictedY));
}
tick++;
}
private void assertState(final @NotNull State expectedState) {
if (expectedState != state) {
player.fail("expected " + expectedState + ", got " + state);
user.fail("expected " + expectedState + ", got " + state);
}
}
private void checkFrame(final boolean condition, final String message) {
if (!condition) {
player.fail(message);
user.fail(message);
throw new CorruptedFrameException(message);
}
}
@@ -361,21 +497,21 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void finish() {
state = State.SUCCESS;
player.getFallback().getConnected().remove(username);
user.getFallback().getConnected().remove(username);
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(
player.getInetAddress().toString(), uuid, login.getStart()
);
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
login.getStart());
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
// Disconnect player with the verification success message
player.disconnect(Sonar.get().getConfig().VERIFICATION_SUCCESS);
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
player.getFallback().getLogger().info(
Sonar.get().getConfig().VERIFICATION_SUCCESSFUL_LOG
.replace("%name%", username)
.replace("%time%", login.formattedDelay())
);
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
}
@@ -21,79 +21,100 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@RequiredArgsConstructor
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackPlayer<?, ?> connection;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private final FallbackUser<?, ?> user;
private final FallbackPacketListener listener;
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackPlayer<?, ?> connection,
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
final @NotNull FallbackPacketListener listener) {
this.connection = connection;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.SERVERBOUND, connection.getProtocolVersion()
);
this.user = user;
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.listener = listener;
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Release the ByteBuf if the connection is not active
// or the ByteBuf doesn't contain any data to avoid
// memory leaks or other potential exploits.
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int originalReaderIndex = byteBuf.readerIndex();
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// If the packet hasn't been found, skip it
// TODO: Can we fail the verification afterwards?
if (packet == null) {
byteBuf.readerIndex(originalReaderIndex);
} else {
try {
doLengthSanityChecks(byteBuf, packet);
try {
packet.decode(byteBuf, connection.getProtocolVersion());
} catch (Throwable throwable) {
connection.fail("could not decode packet");
throw new CorruptedFrameException("Failed to decode packet");
}
if (byteBuf.isReadable()) {
connection.fail("could not read packet to end");
throw new CorruptedFrameException("Could not read packet to end");
}
listener.handle(packet);
} finally {
byteBuf.release();
}
return;
}
try {
// Ensure that the packet isn't too large or too small
doLengthSanityChecks(byteBuf, packet);
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
// Let our verification handler process the packet
listener.handle(packet);
} finally {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
}
} else {
// Packets are always ByteBufs - if we detect an unknown object, fail verification
user.fail("packet is not a ByteBuf");
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
}
}
private void doLengthSanityChecks(final ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, connection.getProtocolVersion());
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
connection.fail("packet too large");
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too large");
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, connection.getProtocolVersion());
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
if (byteBuf.readableBytes() < expectedMinLen) {
connection.fail("packet too small");
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too small");
}
}
@@ -20,19 +20,24 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
private final ProtocolVersion protocolVersion;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion
);
updateRegistry(LOGIN);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
}
@Override
@@ -22,7 +22,11 @@ import io.netty.util.collection.IntObjectMap;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
import java.util.function.Supplier;
@@ -32,16 +36,38 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Most of this is taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
SONAR {
LOGIN {
{
clientbound.fallback = false;
serverbound.fallback = false;
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
map(0x03, MINECRAFT_1_20_2, false));
// LOGIN
clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
clientbound.register(LoginSuccess.class, LoginSuccess::new,
map(0x02, MINECRAFT_1_7_2, false));
}
},
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
map(0x01, MINECRAFT_1_20_2, false));
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(RegistrySync.class, RegistrySync::new,
map(0x05, MINECRAFT_1_20_2, false));
// GAME
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x00, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x01, MINECRAFT_1_20_2, false));
serverbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
}
},
GAME {
{
clientbound.register(JoinGame.class, JoinGame::new,
map(0x01, MINECRAFT_1_7_2, true),
map(0x23, MINECRAFT_1_9, true),
@@ -54,7 +80,8 @@ public enum FallbackPacketRegistry {
map(0x23, MINECRAFT_1_19, true),
map(0x25, MINECRAFT_1_19_1, true),
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true));
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
@@ -67,7 +94,8 @@ public enum FallbackPacketRegistry {
map(0x1E, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false));
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false));
clientbound.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
@@ -80,7 +108,8 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19, true),
map(0x19, MINECRAFT_1_19_1, true),
map(0x17, MINECRAFT_1_19_3, true),
map(0x1A, MINECRAFT_1_19_4, true));
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true));
clientbound.register(PositionLook.class, PositionLook::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
@@ -94,7 +123,8 @@ public enum FallbackPacketRegistry {
map(0x36, MINECRAFT_1_19, true),
map(0x39, MINECRAFT_1_19_1, true),
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true));
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true));
clientbound.register(Abilities.class, Abilities::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
@@ -108,7 +138,8 @@ public enum FallbackPacketRegistry {
map(0x2f, MINECRAFT_1_19, true),
map(0x31, MINECRAFT_1_19_1, true),
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true));
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true));
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
@@ -121,33 +152,21 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, true),
map(0x21, MINECRAFT_1_19_1, true),
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true));
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
map(34, MINECRAFT_1_7_2, true),
map(16, MINECRAFT_1_9, true),
map(15, MINECRAFT_1_13, true),
map(16, MINECRAFT_1_15, true),
map(15, MINECRAFT_1_16, true),
map(59, MINECRAFT_1_16_2, true),
map(63, MINECRAFT_1_17, true),
map(61, MINECRAFT_1_19, true),
map(64, MINECRAFT_1_19_1, true),
map(63, MINECRAFT_1_19_3, true),
map(67, MINECRAFT_1_19_4, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
map(0x46, MINECRAFT_1_12_1, true),
map(0x49, MINECRAFT_1_13, true),
map(0x4D, MINECRAFT_1_14, true),
map(0x4E, MINECRAFT_1_15, true),
map(0x42, MINECRAFT_1_16, true),
map(0x4B, MINECRAFT_1_17, true),
map(0x4A, MINECRAFT_1_19, true),
map(0x4D, MINECRAFT_1_19_1, true),
map(0x4C, MINECRAFT_1_19_3, true),
map(0x50, MINECRAFT_1_19_4, true));
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_13, true),
map(0x10, MINECRAFT_1_15, true),
map(0x0F, MINECRAFT_1_16, true),
map(0x3B, MINECRAFT_1_16_2, true),
map(0x3F, MINECRAFT_1_17, true),
map(0x3D, MINECRAFT_1_19, true),
map(0x40, MINECRAFT_1_19_1, true),
map(0x3F, MINECRAFT_1_19_3, true),
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true));
clientbound.register(Transaction.class, Transaction::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
@@ -159,7 +178,20 @@ public enum FallbackPacketRegistry {
map(0x2D, MINECRAFT_1_19, true),
map(0x2F, MINECRAFT_1_19_1, true),
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true));
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true),
map(0x0E, MINECRAFT_1_16, true),
map(0x0F, MINECRAFT_1_18_2, true),
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
@@ -173,7 +205,8 @@ public enum FallbackPacketRegistry {
map(0x11, MINECRAFT_1_19, false),
map(0x12, MINECRAFT_1_19_1, false),
map(0x11, MINECRAFT_1_19_3, false),
map(0x12, MINECRAFT_1_19_4, false));
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
@@ -183,7 +216,8 @@ public enum FallbackPacketRegistry {
map(0x07, MINECRAFT_1_19, false),
map(0x08, MINECRAFT_1_19_1, false),
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false));
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
@@ -195,7 +229,8 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19, false),
map(0x0D, MINECRAFT_1_19_1, false),
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false));
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false));
serverbound.register(PositionLook.class, PositionLook::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
@@ -208,7 +243,8 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19, false),
map(0x15, MINECRAFT_1_19_1, false),
map(0x14, MINECRAFT_1_19_3, false),
map(0x15, MINECRAFT_1_19_4, false));
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
@@ -223,7 +259,8 @@ public enum FallbackPacketRegistry {
map(0x13, MINECRAFT_1_19, false),
map(0x14, MINECRAFT_1_19_1, false),
map(0x13, MINECRAFT_1_19_3, false),
map(0x14, MINECRAFT_1_19_4, false));
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false));
serverbound.register(Player.class, Player::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
@@ -236,7 +273,8 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_19, false),
map(0x17, MINECRAFT_1_19_1, false),
map(0x16, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false));
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false));
serverbound.register(Transaction.class, Transaction::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
@@ -248,7 +286,8 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x20, MINECRAFT_1_19_4, false));
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false));
}
};
@@ -267,7 +306,6 @@ public enum FallbackPacketRegistry {
public static class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
private boolean fallback = true;
PacketRegistry() {
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
@@ -283,9 +321,6 @@ public enum FallbackPacketRegistry {
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
final ProtocolRegistry registry = versions.get(version);
if (registry == null) {
if (fallback) {
return getProtocolRegistry(MINIMUM_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return registry;
@@ -386,7 +421,7 @@ public enum FallbackPacketRegistry {
return supplier.get();
}
public int getPacketId(final FallbackPacket packet) {
public int getPacketId(final @NotNull FallbackPacket packet) {
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
if (id == Integer.MIN_VALUE) {
@@ -414,9 +449,9 @@ public enum FallbackPacketRegistry {
}
}
private static PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
private static @NotNull PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
return new PacketMapping(id, version, null, encodeOnly);
}
}
@@ -24,29 +24,27 @@ import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.ListBinaryTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
import java.io.InputStream;
import java.util.Collections;
import java.util.Objects;
import java.util.Random;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@UtilityClass
public class FallbackPreparer {
private final Random RANDOM = new Random();
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld",
"sonar", false, false
);
// Mappings
private final CompoundBinaryTag CHAT_TYPE_119;
@@ -61,69 +59,70 @@ public class FallbackPreparer {
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
}
// JoinGame
public JoinGame LEGACY_JOIN_GAME;
private JoinGame JOIN_GAME_1_16;
private JoinGame JOIN_GAME_1_16_2;
private JoinGame JOIN_GAME_1_18_2;
private JoinGame JOIN_GAME_1_19_1;
private JoinGame JOIN_GAME_1_19_4;
private JoinGame JOIN_GAME_1_20;
// Abilities
public final Abilities DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
// Chunks
public final EmptyChunkData EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
public PositionLook SPAWN_TELEPORT;
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
// Registry Sync
public FallbackPacket synchronizeRegistry;
// JoinGame
private FallbackPacket legacyJoinGame;
private FallbackPacket joinGame116;
private FallbackPacket joinGame1162;
private FallbackPacket joinGame1182;
private FallbackPacket joinGame1191;
private FallbackPacket joinGame1194;
private JoinGame joinGame120;
// Dimension
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld", "sonar", false, false);
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
private final int SPAWN_BUFFER = 10; // player spawns at 255 + 10 (10 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is max
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
public int MAX_MOVEMENT_TICK;
public double[] PREPARED_MOVEMENT_PACKETS;
public int DYNAMIC_SPAWN_BUFFER = SPAWN_BUFFER;
public int DYNAMIC_SPAWN_Y_POSITION;
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
public void prepare() {
LEGACY_JOIN_GAME = createLegacyJoinGamePacket();
JOIN_GAME_1_16 = createJoinGamePacket(MINECRAFT_1_16);
JOIN_GAME_1_16_2 = createJoinGamePacket(MINECRAFT_1_16_2);
JOIN_GAME_1_18_2 = createJoinGamePacket(MINECRAFT_1_18_2);
JOIN_GAME_1_19_1 = createJoinGamePacket(MINECRAFT_1_19_1);
JOIN_GAME_1_19_4 = createJoinGamePacket(MINECRAFT_1_19_4);
JOIN_GAME_1_20 = createJoinGamePacket(MINECRAFT_1_20);
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
MAX_MOVEMENT_TICK = Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
PREPARED_MOVEMENT_PACKETS = new double[MAX_MOVEMENT_TICK + 1];
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
maxPredictionTick = maxMovementTick + 10;
preparedCachedYMotions = new double[maxPredictionTick + 1];
for (int i = 0; i < MAX_MOVEMENT_TICK + 1; i++) {
PREPARED_MOVEMENT_PACKETS[i] = -((Math.pow(0.98, i) - 1) * 3.92);
for (int i = 0; i < maxPredictionTick + 1; i++) {
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
}
// Adjust block and collide Y position based on max movement ticks
double maxFallDistance = 0;
for (final double motion : PREPARED_MOVEMENT_PACKETS) {
maxFallDistance += motion;
// Adjust block and collide Y position based on max fall distance
maxFallDistance = 0;
for (int i = 0; i < maxMovementTick; i++) {
maxFallDistance += preparedCachedYMotions[i];
}
// Set the dynamic spawn buffer
DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
// Set the dynamic block and collide Y position based on the maximum fall distance
DYNAMIC_SPAWN_Y_POSITION = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
// Prepare spawn PositionLook with the dynamic Y position
SPAWN_TELEPORT = new PositionLook(
8, DYNAMIC_SPAWN_Y_POSITION, 8,
0f, 0f,
RANDOM.nextInt(Short.MAX_VALUE),
true
);
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
// Prepare collision platform positions
int index = 0;
@@ -135,7 +134,7 @@ public class FallbackPreparer {
z + (BLOCKS_PER_ROW / 2),
0, 0
);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.STONE);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
}
}
@@ -145,60 +144,50 @@ public class FallbackPreparer {
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
return BinaryTagIO.unlimitedReader().read(Objects.requireNonNull(inputStream),
BinaryTagIO.Compression.GZIP
);
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mapping {}", fileName);
throwable.printStackTrace(System.err);
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
return null;
}
}
public static JoinGame getJoinPacketForVersion(final ProtocolVersion protocolVersion) {
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return LEGACY_JOIN_GAME; // 1.7-1.15.2
return legacyJoinGame; // 1.7.2-1.15.2
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return JOIN_GAME_1_16; // 1.16-1.16.1
return joinGame116; // 1.16-1.16.1
}
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
return JOIN_GAME_1_16_2; // 1.16.2-1.18
return joinGame1162; // 1.16.2-1.18
}
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
return JOIN_GAME_1_18_2; // 1.18.1-1.19
return joinGame1182; // 1.18.1-1.19
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return JOIN_GAME_1_19_1; // 1.19.1-1.19.3
return joinGame1191; // 1.19.1-1.19.3
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return JOIN_GAME_1_19_4; // 1.19.4
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
return joinGame1194; // 1.19.4
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return JOIN_GAME_1_20; // 1.20-1.20.1
}
throw new IllegalStateException("Unsupported protocol version");
}
private @NotNull JoinGame createLegacyJoinGamePacket() {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
return joinGame;
return joinGame120; // 1.20-latest
}
// Partially taken from
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
private @NotNull JoinGame createJoinGamePacket(final ProtocolVersion protocolVersion) {
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
// 1.7.2-1.8.9 don't need any dimension information
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
return joinGame;
}
joinGame.setDimensionInfo(DIMENSION);
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
@@ -206,7 +195,7 @@ public class FallbackPreparer {
.add(createDimensionData(protocolVersion))
.build();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
@@ -245,34 +234,30 @@ public class FallbackPreparer {
))).build()
);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) == 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19) == 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
} else if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_4) == 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
}
} else {
registryContainer.put("dimension", encodedDimensionRegistry);
}
try {
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
return joinGame;
}
@@ -289,7 +274,7 @@ public class FallbackPreparer {
.putBoolean("respawn_anchor_works", false)
.putBoolean("has_raids", false)
.putInt("logical_height", 256)
.putString("infiniburn", version.compareTo(ProtocolVersion.MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
":infiniburn_nether" : "minecraft:infiniburn_nether")
.putDouble("coordinate_scale", 1.0)
.putString("effects", "minecraft:overworld")
@@ -299,7 +284,7 @@ public class FallbackPreparer {
.putInt("monster_spawn_light_level", 0)
.build();
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
return CompoundBinaryTag.builder()
.putString("name", "minecraft:overworld")
.putInt("id", 0)
@@ -17,21 +17,69 @@
package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import java.util.function.Function;
@Getter
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE {
@Override
public int getId(final ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(MINECRAFT_1_13) >= 0 ? 1 : 16;
STONE(protocolVersion -> 1),
BARRIER(protocolVersion -> {
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
return 20;
default:
// 1.8-1.12.2
return 166;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 6493;
case MINECRAFT_1_13_2:
return 6494;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
return 7000;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
return 7536;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 7540;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7754;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 8245;
case MINECRAFT_1_19_3:
return 9889;
case MINECRAFT_1_19_4:
return 10221;
case MINECRAFT_1_20:
return 10225;
case MINECRAFT_1_20_2:
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
}
};
});
public abstract int getId(final ProtocolVersion protocolVersion);
private final Function<ProtocolVersion, Integer> function;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
}
}
@@ -1,167 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class JoinGame implements FallbackPacket {
private int entityId;
private short gamemode;
private int dimension;
private long partialHashedSeed; // 1.15+
private short difficulty;
private boolean isHardcore;
private @Nullable String levelType;
private int viewDistance; // 1.14+
private boolean reducedDebugInfo;
private boolean showRespawnScreen;
private String[] levelNames; // 1.16+
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private int portalCooldown; // 1.20+
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
encode116Up(byteBuf, protocolVersion);
} else {
encodeLegacy(byteBuf, protocolVersion);
}
}
private void encodeLegacy(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0) {
byteBuf.writeInt(dimension);
} else {
byteBuf.writeByte(dimension);
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
byteBuf.writeByte(difficulty);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
byteBuf.writeLong(partialHashedSeed);
}
byteBuf.writeByte(1); // max players
writeString(byteBuf, Objects.requireNonNull(levelType));
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, viewDistance);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
byteBuf.writeBoolean(showRespawnScreen);
}
}
private void encode116Up(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
writeCompoundTag(byteBuf, currentDimensionData);
writeString(byteBuf, dimensionInfo.getIdentifier());
} else {
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
}
byteBuf.writeLong(partialHashedSeed);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 1); // max players
} else {
byteBuf.writeByte(1); // max players
}
writeVarInt(byteBuf, viewDistance);
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
writeVarInt(byteBuf, simulationDistance);
}
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
// no last death location
byteBuf.writeBoolean(false);
}
if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, portalCooldown);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -15,34 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Position implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
z = byteBuf.readDouble();
onGround = byteBuf.readBoolean();
}
public final class FinishConfiguration implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0;
}
}
@@ -15,42 +15,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.EncoderException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
import java.io.IOException;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
private int x, y, z;
private float angle;
public final class RegistrySync implements FallbackPacket {
private CompoundBinaryTag dimensionRegistry;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
final long encoded = protocolVersion.compareTo(MINECRAFT_1_14) < 0
? ((x & 0x3FFFFFFL) << 38) | ((y & 0xFFFL) << 26) | (z & 0x3FFFFFFL)
: ((x & 0x3FFFFFFL) << 38) | ((y & 0x3FFFFFFL) << 12) | (z & 0xFFFL);
byteBuf.writeLong(encoded);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
byteBuf.writeFloat(angle);
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
stream.writeByte(10); // CompoundTag ID
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
} catch (IOException exception) {
throw new EncoderException("Cannot write NBT CompoundTag");
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
public final class LoginAcknowledged implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
}
@@ -15,12 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
@@ -34,12 +35,12 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public final class ServerLoginSuccess implements FallbackPacket {
public final class LoginSuccess implements FallbackPacket {
private String username;
private UUID uuid;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeUUID(byteBuf, uuid);
} else if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -34,14 +35,14 @@ public class Abilities implements FallbackPacket {
private float flySpeed, walkSpeed;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeByte(encodedFlags);
byteBuf.writeFloat(flySpeed);
byteBuf.writeFloat(walkSpeed);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
encodedFlags = byteBuf.readByte();
flySpeed = byteBuf.readFloat();
walkSpeed = byteBuf.readFloat();
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Chat implements FallbackPacket {
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
private Component component;
private byte position;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
final String serialized = JSONComponentSerializer.json().serialize(component);
writeString(byteBuf, serialized);
// Message type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
byteBuf.writeBoolean(position == 2);
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeVarInt(byteBuf, position);
} else {
byteBuf.writeByte(this.position);
}
// UUID
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
writeUUID(byteBuf, PLACEHOLDER_UUID);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -55,7 +56,7 @@ public final class ClientSettings implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
locale = readString(byteBuf, 16);
viewDistance = byteBuf.readByte();
chatVisibility = readVarInt(byteBuf);
@@ -15,20 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.Objects;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
@Getter
@@ -38,17 +40,17 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
public final class Disconnect implements FallbackPacket {
private @Nullable String reason;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
reason = readString(byteBuf);
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
writeString(byteBuf, Objects.requireNonNull(reason));
}
public static Disconnect create(final String serialized) {
return new Disconnect(serialized);
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
}
}
@@ -15,28 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.LongArrayBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.BitSet;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Partially taken from
// https://github.com/Leymooo/BungeeCord/blob/master/protocol/src/main/java/ru/leymooo/botfilter/packets/EmptyChunkPacket.java
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class EmptyChunkData implements FallbackPacket {
@@ -45,8 +43,9 @@ public final class EmptyChunkData implements FallbackPacket {
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
private static final byte[] LEGACY_FILLER_BYTES_17 = new byte[2];
private static final byte[] LEGACY_FILLER_BYTES = new byte[256];
private static final byte[] MODERN_FILLER_BYTES = new byte[1024];
private static final byte[] MODERN_FILLER_BYTES = new byte[256 * 4];
// Prepare nbt for 1.18 and pre-1.18
private static final CompoundBinaryTag MODERN_TAG, LEGACY_TAG;
@@ -57,48 +56,33 @@ public final class EmptyChunkData implements FallbackPacket {
}
private static @NotNull CompoundBinaryTag prepareNBT(final boolean legacy) {
final long[] arrayData = new long[legacy ? 36 : 37];
final LongArrayBinaryTag longArray = LongArrayBinaryTag.longArrayBinaryTag(arrayData);
final CompoundBinaryTag motion = CompoundBinaryTag.builder()
.put("MOTION_BLOCKING", longArray)
.build();
return CompoundBinaryTag.builder()
.put("root", motion)
.put("root", CompoundBinaryTag.builder()
.put("MOTION_BLOCKING", LongArrayBinaryTag.longArrayBinaryTag(new long[legacy ? 36 : 37]))
.build())
.build();
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, 0); // mask
} else {
byteBuf.writeBoolean(true); // full chunk
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0 && protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // ignore old data
}
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) == 0) {
byteBuf.writeShort(1);
} else {
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, 0);
}
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
final BitSet bitSet = new BitSet();
for (int i = 0; i < 16; i++) {
bitSet.set(i, false);
}
long[] mask = bitSet.toLongArray();
writeVarInt(byteBuf, mask.length);
for (long l : mask) {
byteBuf.writeLong(l);
} else {
byteBuf.writeShort(1); // fix void chunk
}
}
@@ -121,7 +105,12 @@ public final class EmptyChunkData implements FallbackPacket {
}
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
} else {
byteBuf.writeInt(0); // compressed size
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
}
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class JoinGame implements FallbackPacket {
private int entityId;
private short gamemode;
private int dimension;
private long partialHashedSeed; // 1.15+
private short difficulty;
private boolean isHardcore;
private @Nullable String levelType;
private int viewDistance; // 1.14+
private boolean reducedDebugInfo;
private boolean showRespawnScreen;
private boolean limitedCrafting; // 1.20.2+
private String[] levelNames; // 1.16+
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_9) <= 0) {
byteBuf.writeByte(gamemode);
byteBuf.writeByte(dimension);
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(1); // max players
writeString(byteBuf, "flat"); // level type
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(reducedDebugInfo);
}
}
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(dimension);
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(1); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(dimension);
byteBuf.writeByte(1); // max players
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(dimension);
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(1); // max players
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
}
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(1); // max players
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
}
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
writeCompoundTag(byteBuf, currentDimensionData);
writeString(byteBuf, dimensionInfo.getIdentifier());
byteBuf.writeLong(partialHashedSeed);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, viewDistance);
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
writeVarInt(byteBuf, viewDistance); // simulation distance
}
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
}
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeLong(partialHashedSeed);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // last death location
if (protocolVersion.compareTo(MINECRAFT_1_20) == 0) {
writeVarInt(byteBuf, 0); // portal cooldown
}
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(limitedCrafting);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // last death location
writeVarInt(byteBuf, 0); // portal cooldown
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -15,12 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -30,13 +32,14 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class KeepAlive implements FallbackPacket {
private long id;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
byteBuf.writeLong(id);
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -47,7 +50,7 @@ public final class KeepAlive implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
id = byteBuf.readLong();
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -33,7 +34,7 @@ public final class Player implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
onGround = byteBuf.readBoolean();
}
@@ -15,11 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
@@ -45,7 +46,12 @@ public final class PluginMessage extends DeferredByteBufHolder implements Fallba
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xfff + 1; // limit packet size to 4096
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Position implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.skipBytes(8);
}
y = byteBuf.readDouble();
z = byteBuf.readDouble();
onGround = byteBuf.readBoolean();
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
}
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 25;
}
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -39,8 +40,12 @@ public final class PositionLook implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.skipBytes(8);
}
y = byteBuf.readDouble();
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
@@ -49,20 +54,36 @@ public final class PositionLook implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
byteBuf.writeFloat(yaw);
byteBuf.writeFloat(pitch);
byteBuf.writeByte(0x00);
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
writeVarInt(byteBuf, teleportId);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeBoolean(onGround);
} else {
byteBuf.writeByte(0x00);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0 && protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
byteBuf.writeBoolean(true); // Dismount vehicle
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
writeVarInt(byteBuf, teleportId);
}
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
byteBuf.writeBoolean(true); // Always dismount vehicle
}
}
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 41 : 33;
}
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 33;
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -36,12 +37,12 @@ public final class Transaction implements FallbackPacket {
private boolean accepted;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeByte(windowId);
byteBuf.writeShort((short) id);
// The "accepted" field is actually really unnecessary since
// the client must accept the transaction either way.
// it's never even used in the client.
byteBuf.writeBoolean(accepted);
} else {
byteBuf.writeInt(id);
@@ -49,7 +50,7 @@ public final class Transaction implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
windowId = byteBuf.readByte();
id = byteBuf.readShort();
@@ -15,51 +15,61 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_16_2;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarLong;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"changedBlocks"})
public final class UpdateSectionBlocks implements FallbackPacket {
private int sectionX, sectionZ;
private ChangedBlock[] changedBlocks;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeInt(sectionX);
byteBuf.writeInt(sectionZ);
writeVarInt(byteBuf, changedBlocks.length);
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(changedBlocks.length);
byteBuf.writeInt(4 * changedBlocks.length);
} else {
writeVarInt(byteBuf, changedBlocks.length);
}
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
writeVarInt(byteBuf, block.getType().getId(protocolVersion));
final int id = block.getType().getId(protocolVersion);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
writeVarInt(byteBuf, id);
} else {
final int shiftedBlockId = id << 4;
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(shiftedBlockId);
} else {
writeVarInt(byteBuf, shiftedBlockId);
}
}
}
} else {
final ChangedBlock lastChangedBlock = changedBlocks[changedBlocks.length - 1];
final int sectionY = lastChangedBlock.getPosition().getY() >> 4;
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
// Why is Mojang doing this? :(
byteBuf.writeLong(
((long) (sectionX & 0x3FFFFF) << 42)
| (sectionY & 0xFFFFF)
| ((long) (sectionZ & 0x3FFFFF) << 20)
);
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
// 1.20+ don't have light update suppression
if (protocolVersion.compareTo(MINECRAFT_1_20) < 0) {
@@ -70,7 +80,8 @@ public final class UpdateSectionBlocks implements FallbackPacket {
for (final ChangedBlock block : changedBlocks) {
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
writeVarLong(byteBuf, shiftedBlockId | block.getModernChunkPosCrammed());
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
writeVarLong(byteBuf, positionIdValue);
}
}
}
@@ -27,13 +27,11 @@ public class TrafficChannelHooker implements FallbackPipelines {
public void hook(final @NotNull ChannelPipeline pipeline,
final @NotNull String decoder,
final @NotNull String encoder) {
pipeline.channel().eventLoop().execute(() -> {
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
});
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
}
}
@@ -25,7 +25,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
@SubcommandInfo(
name = "blacklist",
@@ -44,81 +43,56 @@ public final class BlacklistCommand extends Subcommand {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "add": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist add <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
return;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
// Make sure the IP is not blacklisted already
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_DUPLICATE);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
return;
}
// Display a warning if the IP is verified but being added to the blacklist
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD_WARNING
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
// Blacklist the given IP address
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist remove <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
return;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
// Make sure the IP is blacklisted
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
return;
}
// Invalidate the cache entry of the blacklisted IP address
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_REMOVE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -127,21 +101,19 @@ public final class BlacklistCommand extends Subcommand {
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
if (blacklisted == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_EMPTY);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
return;
}
// Invalidate all cache entries
SONAR.getFallback().getBlacklisted().invalidateAll();
invocation.getSender().sendMessage(
SONAR.getConfig().BLACKLIST_CLEARED
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted))
);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
break;
}
@@ -17,38 +17,101 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
// This command is only used to display helpful information in order to help
// the Sonar developer(s) to fix issues more quickly
// the Sonar contributors to fix issues more quickly
@SubcommandInfo(
name = "dump",
description = "Print developer information",
onlyConsole = true
)
public final class DumpCommand extends Subcommand implements JVMProfiler {
private static final Gson GSON = new GsonBuilder()
.disableInnerClassSerialization()
.create();
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String osName = System.getProperty("os.name");
final String osArch = System.getProperty("os.arch");
final String jvmArgs = String.join(",", RUNTIME_MX_BEAN.getInputArguments());
final String json = GSON.toJson(collectMappedInformation());
SONAR.getLogger().info("Generated dump: {}", json);
}
invocation.getSender().sendMessage("Virtual cores: " + getVirtualCores());
invocation.getSender().sendMessage("JVM args: " + jvmArgs);
invocation.getSender().sendMessage("Java name: " + RUNTIME_MX_BEAN.getVmName());
invocation.getSender().sendMessage("Java version: " + RUNTIME_MX_BEAN.getVmVersion());
invocation.getSender().sendMessage("Java vendor: " + RUNTIME_MX_BEAN.getVmVendor());
invocation.getSender().sendMessage("Operating system: " + osName + ", " + osArch);
invocation.getSender().sendMessage("Total memory: " + formatMemory(getTotalMemory()));
invocation.getSender().sendMessage("Max memory: " + formatMemory(getMaxMemory()));
invocation.getSender().sendMessage("Free memory: " + formatMemory(getFreeMemory()));
invocation.getSender().sendMessage("Used Memory: " + formatMemory(getUsedMemory()));
invocation.getSender().sendMessage("Platform: " + SONAR.getServer().getPlatform().getDisplayName());
invocation.getSender().sendMessage("Version: " + SONAR.getVersion().getFull());
@Unmodifiable
private @NotNull Map<String, Object> collectMappedInformation() {
val mappings = new WeakHashMap<String, Object>();
mappings.put("sonar", new Dump.Sonar(
SONAR.getVersion().getFull(),
SONAR.getServer().getPlatform(),
SONAR.getVersion().isOnMainBranch()
));
mappings.put("runtime", new Dump.Runtime(
getVirtualCores(),
RUNTIME_MX_BEAN.getInputArguments(),
RUNTIME_MX_BEAN.getVmName(),
RUNTIME_MX_BEAN.getVmVendor(),
RUNTIME_MX_BEAN.getVmVersion()
));
mappings.put("os", new Dump.OS(
System.getProperty("os.name"),
System.getProperty("os.arch"),
System.getProperty("os.version")
));
mappings.put("memory", new Dump.Memory(
formatMemory(getTotalMemory()),
formatMemory(getMaxMemory()),
formatMemory(getFreeMemory()),
formatMemory(getUsedMemory())
));
return mappings;
}
@SuppressWarnings("unused")
private static final class Dump {
@RequiredArgsConstructor
private static final class Sonar {
private final String version;
private final SonarPlatform platform;
private final boolean isOnMainBranch;
}
@RequiredArgsConstructor
private static final class Runtime {
private final int virtualCores;
private final List<String> jvmArguments;
private final String vmName;
private final String vmVendor;
private final String vmVersion;
}
@RequiredArgsConstructor
private static final class Memory {
private final String total;
private final String max;
private final String free;
private final String used;
}
@RequiredArgsConstructor
private static final class OS {
private final String name;
private final String arch;
private final String version;
}
}
}
@@ -24,20 +24,18 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@SubcommandInfo(
name = "lockdown",
description = "Lock the server down"
description = "Prevent all players from joining"
)
public final class LockdownCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
SONAR.getConfig().LOCKDOWN_ENABLED = !SONAR.getConfig().LOCKDOWN_ENABLED;
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", SONAR.getConfig().LOCKDOWN_ENABLED);
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
SONAR.getConfig().getLockdown().setEnabled(newState);
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", newState);
SONAR.getConfig().getGeneralConfig().save();
invocation.getSender().sendMessage(
SONAR.getConfig().LOCKDOWN_ENABLED
? SONAR.getConfig().LOCKDOWN_ACTIVATED
: SONAR.getConfig().LOCKDOWN_DEACTIVATED
);
invocation.getSender().sendMessage(newState
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
}
}
@@ -21,24 +21,22 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@SubcommandInfo(
name = "reload",
description = "Reload the configuration"
description = "Reload all configurations"
)
public final class ReloadCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final long startTime = System.currentTimeMillis();
final SystemTimer timer = new SystemTimer();
invocation.getSender().sendMessage(SONAR.getConfig().RELOADING);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
SONAR.reload();
final long timeTaken = System.currentTimeMillis() - startTime;
invocation.getSender().sendMessage(
SONAR.getConfig().RELOADED
.replace("%taken%", String.valueOf(timeTaken))
);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloaded()
.replace("%taken%", String.valueOf(timer.delay())));
}
}
@@ -40,60 +40,71 @@ import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
argumentsRequired = false
)
public final class StatisticsCommand extends Subcommand implements JVMProfiler {
private enum StatisticType {
GENERAL,
NETWORK,
MEMORY,
CPU
}
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String showing = invocation.getRawArguments().length >= 2
? invocation.getRawArguments()[1].toLowerCase() : "bot";
invocation.getSender().sendMessage("§eShowing " + showing + " statistics for this session:");
invocation.getSender().sendMessage("§ePlease note that session statistics are not saved when the server restarts.");
invocation.getSender().sendMessage();
if (!showing.equals("bot")) {
switch (showing) {
case "network": {
invocation.getSender().sendMessage(" §7▪ §aCurrent incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedSecond());
invocation.getSender().sendMessage(" §7▪ §aCurrent outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedSecond());
invocation.getSender().sendMessage(" §7▪ §aTotal incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedTtl());
invocation.getSender().sendMessage(" §7▪ §aTotal outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedTtl());
return;
}
case "memory": {
invocation.getSender().sendMessage(" §7▪ §aTotal free memory (JVM): §f" + formatMemory(getFreeMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal used memory (JVM): §f" + formatMemory(getUsedMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal maximum memory (JVM): §f" + formatMemory(getMaxMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal allocated memory (JVM): §f" + formatMemory(getTotalMemory()));
return;
}
case "cpu": {
invocation.getSender().sendMessage(" §7▪ §aProcess CPU usage right now: §f" + DECIMAL_FORMAT.format(getProcessCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aSystem CPU usage right now: §f" + DECIMAL_FORMAT.format(getSystemCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aPer-core process CPU usage: §f" + DECIMAL_FORMAT.format(getAverageProcessCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aPer-core system CPU usage: §f" + DECIMAL_FORMAT.format(getAverageSystemCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aGeneral system load average: §f" + DECIMAL_FORMAT.format(getSystemLoadAverage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aTotal virtual cpu cores (JVM): §f" + DECIMAL_FORMAT.format(getVirtualCores()));
return;
}
StatisticType type = StatisticType.GENERAL;
if (invocation.getRawArguments().length >= 2) {
try {
type = StatisticType.valueOf(invocation.getRawArguments()[1].toUpperCase());
} catch (Exception exception) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getUnknownStatisticType()
.replace("%statistics%", getArguments()));
return;
}
}
final int total = Statistics.TOTAL_TRAFFIC.get();
final int real = Statistics.REAL_TRAFFIC.get();
final int failed = Statistics.FAILED_VERIFICATIONS.get();
final int queued = SONAR.getFallback().getQueue().getQueuedPlayers().size();
final int verifying = SONAR.getFallback().getConnected().size();
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
.replace("%type%", type.name().toLowerCase()));
invocation.getSender().sendMessage();
invocation.getSender().sendMessage(" §7▪ §aVerified IP addresses: §f" + DECIMAL_FORMAT.format(verified));
invocation.getSender().sendMessage(" §7▪ §aVerifying IP addresses: §f" + DECIMAL_FORMAT.format(verifying));
invocation.getSender().sendMessage(" §7▪ §aBlacklisted IP addresses: §f" + DECIMAL_FORMAT.format(blacklisted));
invocation.getSender().sendMessage(" §7▪ §aCurrently queued logins: §f" + DECIMAL_FORMAT.format(queued));
invocation.getSender().sendMessage(" §7▪ §aTotal non-unique joins: §f" + DECIMAL_FORMAT.format(total));
invocation.getSender().sendMessage(" §7▪ §aTotal verification attempts: §f" + DECIMAL_FORMAT.format(real));
invocation.getSender().sendMessage(" §7▪ §aTotal failed verifications: §f" + DECIMAL_FORMAT.format(failed));
switch (type) {
case GENERAL: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize()))
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
.replace("%total_joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%total_attempts%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%total_failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get())));
break;
}
case CPU: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getCpuStatistics()
.replace("%process_cpu%", DECIMAL_FORMAT.format(getProcessCPUUsage()))
.replace("%system_cpu%", DECIMAL_FORMAT.format(getSystemCPUUsage()))
.replace("%average_process_cpu%", DECIMAL_FORMAT.format(getAverageProcessCPUUsage()))
.replace("%average_system_cpu%", DECIMAL_FORMAT.format(getAverageSystemCPUUsage()))
.replace("%load_average%", DECIMAL_FORMAT.format(getSystemLoadAverage()))
.replace("%virtual_cores%", DECIMAL_FORMAT.format(getVirtualCores())));
break;
}
case MEMORY: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getMemoryStatistics()
.replace("%free_memory%", formatMemory(getFreeMemory()))
.replace("%used_memory%", formatMemory(getUsedMemory()))
.replace("%max_memory%", formatMemory(getMaxMemory()))
.replace("%total_memory%", formatMemory(getTotalMemory())));
break;
}
case NETWORK: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
.replace("%incoming%", TrafficCounter.INCOMING.getCachedSecond())
.replace("%outgoing%", TrafficCounter.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", TrafficCounter.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", TrafficCounter.OUTGOING.getCachedTtl()));
break;
}
}
}
}
@@ -19,7 +19,7 @@ package xyz.jonesdev.sonar.common.subcommand.impl;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@@ -34,33 +34,29 @@ public final class VerboseCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
InvocationSender verboseSubscriber = invocation.getSender();
InvocationSource verboseSubscriber = invocation.getSender();
// Support for '/sonar verbose [username]'
if (invocation.getRawArguments().length >= 2) {
final Optional<InvocationSender> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
final Optional<InvocationSource> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
if (optional.isPresent()) verboseSubscriber = optional.get();
}
if (SONAR.getVerboseHandler().isSubscribed(verboseSubscriber.getName())) {
SONAR.getVerboseHandler().unsubscribe(verboseSubscriber.getName());
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_UNSUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_UNSUBSCRIBED);
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
return;
}
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_SUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_SUBSCRIBED);
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
}
}
@@ -24,6 +24,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
@@ -39,75 +40,70 @@ import java.util.concurrent.LinkedBlockingQueue;
}
)
public final class VerifiedCommand extends Subcommand {
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(10);
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
@Override
public void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified history <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
// Make sure the IP is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
return;
}
invocation.getSender().sendMessage("§ePrevious UUIDs for " + inetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(realInetAddress)) {
invocation.getSender().sendMessage(" §7▪ §f" + uuid.toString());
invocation.getSender().sendMessage("<yellow>Previous UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(inetAddress.toString())) {
invocation.getSender().sendMessage(" <gray>▪ <white>" + uuid.toString());
}
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified remove <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
if (LOCK.contains(rawInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (LOCK.contains(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_BLOCKED);
return;
}
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
// Make sure the player is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
return;
}
// Lock the IP address
// Make sure we don't accidentally run 2 operations at the same time
LOCK.add(realInetAddress);
SONAR.getVerifiedPlayerController().remove(realInetAddress);
LOCK.remove(realInetAddress);
LOCK.add(rawInetAddress);
SONAR.getVerifiedPlayerController().remove(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_REMOVE
.replace("%ip%", inetAddress));
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedRemove()
.replace("%ip%", rawInetAddress));
// Unlock the IP address
LOCK.remove(rawInetAddress);
break;
}
@@ -115,19 +111,19 @@ public final class VerifiedCommand extends Subcommand {
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
if (verified == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_EMPTY);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedEmpty());
return;
}
// Invalidate all cache entries
SONAR.getVerifiedPlayerController().clearAll();
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_CLEARED
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
break;
}
@@ -0,0 +1,98 @@
package xyz.jonesdev.sonar.common.update;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@UtilityClass
public class UpdateChecker {
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
private final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[update-checker] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[update-checker] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[update-checker] " + message, args);
}
};
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
try {
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
final HttpsURLConnection urlConnection = prepareConnection(url);
final JsonObject json = parseJson(urlConnection.getInputStream());
final String latestStableVersion = json.get("tag_name").getAsString();
if (!latestStableVersion.equals(Sonar.get().getVersion().getSemanticVersion())) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableVersion);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases");
} else if (Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.info("You are currently using the latest stable release of Sonar!");
} else {
LOGGER.warn("You are currently using the latest development version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version" +
".");
}
} catch (Exception exception) {
LOGGER.warn("Could not retrieve latest version information: {}", exception);
}
});
}
private @NotNull JsonObject parseJson(final @NotNull InputStream inputStream) throws IOException {
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
final StringBuilder content = new StringBuilder();
// Read the entire page content
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
content.append(line);
}
}
// Parse site content as JSON using Gson
final JsonParser jsonParser = new JsonParser();
return jsonParser.parse(content.toString()).getAsJsonObject();
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// Set connection timeout
urlConnection.setConnectTimeout(15000);
urlConnection.setReadTimeout(15000);
// Set necessary properties
urlConnection.setRequestMethod("GET");
urlConnection.setRequestProperty("Accept", "application/vnd.github.v3+json");
// Only continue if the connection is a success
if (urlConnection.getResponseCode() != 200) {
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
}
return urlConnection;
}
}
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTagIO;
@@ -33,7 +34,7 @@ import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
// Mostly taken from
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
public class ProtocolUtil {
@@ -47,40 +48,41 @@ public class ProtocolUtil {
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
public static @NotNull String readString(final ByteBuf buf) throws CorruptedFrameException {
return readString(buf, Short.MAX_VALUE);
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
final ByteBuf slice = content.slice();
try {
return readString(slice);
} catch (DecoderException exception) {
return readString(slice, 65536, slice.readableBytes());
}
}
public static @NotNull String readString(final ByteBuf buf,
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
return readString(byteBuf, Short.MAX_VALUE);
}
public static @NotNull String readString(final ByteBuf byteBuf,
final int cap) throws CorruptedFrameException {
return readString(buf, cap, false);
final int length = VarIntUtil.readVarInt(byteBuf);
return readString(byteBuf, cap, length);
}
public static @NotNull String readString(final ByteBuf buf,
public static @NotNull String readString(final @NotNull ByteBuf byteBuf,
final int cap,
final boolean legacy) throws CorruptedFrameException {
final int length = VarIntUtil.readVarInt(buf);
final int length) throws CorruptedFrameException {
checkFrame(length >= 0, "Got a negative-length string");
checkFrame(length <= cap * 3, "Bad string size");
String str;
if (legacy) {
// TODO: length checking?
str = buf.toString(StandardCharsets.UTF_8);
buf.skipBytes(buf.readableBytes());
} else {
checkFrame(buf.isReadable(length), "Got an invalid string length");
str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
}
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
byteBuf.skipBytes(length);
checkFrame(str.length() <= cap, "Got a too-long string");
return str;
}
public static void writeString(final ByteBuf buf, final CharSequence str) {
public static void writeString(final ByteBuf byteBuf, final CharSequence str) {
final int size = ByteBufUtil.utf8Bytes(str);
VarIntUtil.writeVarInt(buf, size);
buf.writeCharSequence(str, StandardCharsets.UTF_8);
VarIntUtil.writeVarInt(byteBuf, size);
byteBuf.writeCharSequence(str, StandardCharsets.UTF_8);
}
public static void writeUUID(final @NotNull ByteBuf byteBuf, final @NotNull UUID uuid) {
+20 -3
View File
@@ -4,17 +4,34 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion") // Proxy module
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
testCompileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
compileOnly("com.velocitypowered:velocity-api:$velocityVersion")
annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
//annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
testCompileOnly("com.velocitypowered:velocity-api:$velocityVersion")
testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
//testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-velocity:3.0.2")
}
tasks {
processResources {
val props = mapOf(
"name" to rootProject.name,
"version" to rootProject.version.toString().split("-")[0],
"description" to "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)",
"url" to "https://jonesdev.xyz/discord/",
"main" to "xyz.jonesdev.sonar.velocity.SonarVelocityPlugin"
)
inputs.properties(props)
filesMatching("velocity-plugin.json") {
expand(props)
}
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -18,15 +18,15 @@
package xyz.jonesdev.sonar.velocity;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.velocity.command.SonarCommand;
import xyz.jonesdev.sonar.velocity.command.VelocityInvocationSource;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
import xyz.jonesdev.sonar.velocity.verbose.VerboseWrapper;
@@ -72,30 +72,14 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
public final ServerWrapper server = new ServerWrapper(SonarPlatform.VELOCITY) {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.VELOCITY;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getAllPlayers().stream()
.filter(player -> player.getUsername().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getUsername();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(Component.text(message));
}
});
.map(VelocityInvocationSource::new);
}
};
@@ -106,7 +90,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
getPlugin().getMetricsFactory().make(getPlugin(), getServer().getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new SonarCommand());
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener(getFallback()));
@@ -126,11 +110,4 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
.repeat(100L, TimeUnit.MILLISECONDS)
.schedule();
}
@Override
public void postReload() {
// Prepare cached messages
FallbackListener.CachedMessages.update();
}
}
@@ -31,9 +31,10 @@ import org.slf4j.Logger;
import java.nio.file.Path;
@Getter
@Plugin(id = "sonar",
@Plugin(
id = "sonar",
name = "Sonar",
version = "2.0.0",
version = "$version",
authors = {"Jones Development", "Sonar Contributors"},
url = "https://jonesdev.xyz/discord/",
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player
? ((Player) sender).getUsername()
: "Console", sender);
}
}
@@ -21,14 +21,11 @@ import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
@@ -39,54 +36,22 @@ import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
Component.text("Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ ".", NamedTextColor.YELLOW),
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
Component.empty(),
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
Component.textOfChildren(
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
var component = Component.textOfChildren(
Component.text(" ▪ ", NamedTextColor.GRAY),
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
Component.text(" - ", NamedTextColor.GRAY),
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
);
component = component.clickEvent(
ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
).hoverEvent(
HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
))
);
CACHED_HELP_MESSAGE.add(component);
});
public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
{
cacheHelpMessage();
}
@Override
public void execute(final @NotNull Invocation invocation) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (!(invocation.source() instanceof ConsoleCommandSource)) {
// Check if the player actually has the permission to run the command
if (!invocation.source().hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
@@ -96,16 +61,14 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().COMMAND_COOL_DOWN));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocation.source().sendMessage(Component.text(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
@@ -114,21 +77,6 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return invocation.source() instanceof Player
? ((Player) invocation.source()).getUsername()
: "Console";
}
@Override
public void sendMessage(final String message) {
invocation.source().sendMessage(Component.text(message));
}
};
if (invocation.arguments().length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -142,10 +90,8 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
if (!subcommand.get().getInfo().onlyConsole()
&& !invocation.source().hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
.replace("%permission%", subcommand.get().getPermission())
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
@@ -153,25 +99,23 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(invocation.source() instanceof Player)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(invocation.source() instanceof ConsoleCommandSource)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, invocation.arguments());
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, invocation.arguments());
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1
&& sub.getInfo().argumentsRequired()) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -184,14 +128,18 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
invocation.source().sendMessage((Component) component);
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
}
@Override
public List<String> suggest(final @NotNull Invocation invocation) {
// Do not allow tab completion if the player does not have the required permission
if (!invocation.source().hasPermission("sonar.command")) {
return emptyList();
}
if (invocation.arguments().length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
@@ -221,9 +169,4 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
}
return emptyList();
}
@Override
public boolean hasPermission(final @NotNull Invocation invocation) {
return invocation.source().hasPermission("sonar.command");
}
}
@@ -21,7 +21,9 @@ import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
@@ -37,62 +39,71 @@ import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
import xyz.jonesdev.sonar.velocity.fallback.dummy.DummyConnection;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
import static xyz.jonesdev.sonar.velocity.fallback.FallbackListener.CachedMessages.*;
@RequiredArgsConstructor
public final class FallbackListener {
private final @NotNull Fallback fallback;
public static class CachedMessages {
static Component LOCKDOWN_DISCONNECT;
static Component TOO_MANY_ONLINE_PER_IP;
static Component TOO_MANY_PLAYERS;
static Component BLACKLISTED;
static Component ALREADY_VERIFYING;
static Component ALREADY_QUEUED;
static Component TOO_FAST_RECONNECT;
public static Component INVALID_USERNAME;
public static void update() {
ALREADY_VERIFYING = Component.text(Sonar.get().getConfig().ALREADY_VERIFYING);
ALREADY_QUEUED = Component.text(Sonar.get().getConfig().ALREADY_QUEUED);
TOO_MANY_PLAYERS = Component.text(Sonar.get().getConfig().TOO_MANY_PLAYERS);
BLACKLISTED = Component.text(Sonar.get().getConfig().BLACKLISTED);
TOO_FAST_RECONNECT = Component.text(Sonar.get().getConfig().TOO_FAST_RECONNECT);
INVALID_USERNAME = Component.text(Sonar.get().getConfig().INVALID_USERNAME);
TOO_MANY_ONLINE_PER_IP = Component.text(Sonar.get().getConfig().TOO_MANY_ONLINE_PER_IP);
LOCKDOWN_DISCONNECT = Component.text(Sonar.get().getConfig().LOCKDOWN_DISCONNECT);
}
}
private static final DummyConnection CLOSED_MINECRAFT_CONNECTION;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodHandle INITIAL_CONNECTION;
private static final MethodHandle CONNECTED_PLAYER;
private static final Field CONNECTION_FIELD;
private static Field SESSION_HANDLER_FIELD;
static {
CLOSED_MINECRAFT_CONNECTION = new DummyConnection(null);
try {
CONNECTED_PLAYER = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
.findConstructor(ConnectedPlayer.class,
MethodType.methodType(
void.class,
VelocityServer.class,
GameProfile.class,
MinecraftConnection.class,
InetSocketAddress.class,
boolean.class,
IdentifiedKey.class
)
);
try {
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("activeSessionHandler");
} catch (Throwable throwable) {
// Velocity b266 changed the field name to "activeSessionHandler"
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("sessionHandler");
}
SESSION_HANDLER_FIELD.setAccessible(true);
CONNECTION_FIELD = InitialLoginSessionHandler.class.getDeclaredField("mcConnection");
CONNECTION_FIELD.setAccessible(true);
@@ -117,84 +128,11 @@ public final class FallbackListener {
public void handle(final @NotNull PreLoginEvent event) throws Throwable {
Statistics.TOTAL_TRAFFIC.increment();
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
val inboundConnection = (LoginInboundConnection) event.getConnection();
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
final MinecraftConnection mcConnection = initialConnection.getConnection();
final Channel channel = mcConnection.getChannel();
final ChannelPipeline pipeline = channel.pipeline();
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
// Check the blacklist here since we cannot let the player "ghost join"
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(mcConnection.getSessionHandler());
initialConnection.getConnection().closeWith(Disconnect.create(
BLACKLISTED,
inboundConnection.getProtocolVersion()
));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().ENABLE_VERIFICATION) return;
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Completely skip Geyser connections (for now)
if (isGeyserConnection(channel)) {
// TODO: Do we need to log this?
fallback.getLogger().info("Allowing Geyser connection: " + inetAddress);
return;
}
// We now mark the connection as dead by using our dummy connection
markConnectionAsDead(mcConnection.getSessionHandler());
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
ALREADY_VERIFYING,
inboundConnection.getProtocolVersion()
));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().MAXIMUM_VERIFYING_PLAYERS) {
initialConnection.getConnection().closeWith(Disconnect.create(
TOO_MANY_PLAYERS,
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
TOO_FAST_RECONNECT,
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
ALREADY_QUEUED,
inboundConnection.getProtocolVersion()
));
return;
}
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
@@ -202,47 +140,204 @@ public final class FallbackListener {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
// Queue the connection for further processing
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
try {
// Backwards compatibility for Velocity b265 and below
val activeSessionHandler = (MinecraftSessionHandler) SESSION_HANDLER_FIELD.get(mcConnection);
// Check if the username matches the valid name regex in order to prevent
// UTF-16 names or other types of flood attacks
if (!Sonar.get().getConfig().VALID_NAME_REGEX
.matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(INVALID_USERNAME, mcConnection.getProtocolVersion()));
// Check the blacklist here since we cannot let the player "ghost join"
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(activeSessionHandler);
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(
Sonar.get().getConfig().VERIFICATION_READ_TIMEOUT,
TimeUnit.MILLISECONDS
)
);
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().getVerification().isEnabled()) return;
// We need to determine if the player is premium before we handle the connection,
// so we can create a ConnectedPlayer instance without having to spoof this
final boolean onlineMode = !event.getResult().isForceOfflineMode()
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|| event.getResult().isOnlineModeAllowed());
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Replace the session handler to intercept all packets and handle them
mcConnection.setSessionHandler(new FallbackSessionHandler(
fallback, mcConnection, inboundConnection,
gameProfile, inetAddress, onlineMode
));
}));
// Completely skip Geyser connections (for now)
if (isGeyserConnection(channel)) {
// TODO: Do we need to log this?
fallback.getLogger().info("Allowing Geyser connection: {}", inetAddress);
return;
}
// We now mark the connection as dead by using our fake connection
markConnectionAsDead(activeSessionHandler);
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
// Queue the connection for further processing
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
mcConnection.getProtocolVersion()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
TimeUnit.MILLISECONDS)
);
// We need to determine if the player is premium before we handle the connection,
// so we can create a ConnectedPlayer instance without having to spoof this
final boolean onlineMode = !event.getResult().isForceOfflineMode()
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|| event.getResult().isOnlineModeAllowed());
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Create an instance for the connected player
final ConnectedPlayer connectedPlayer;
try {
connectedPlayer = (ConnectedPlayer) CONNECTED_PLAYER.invokeExact(
mcConnection.server,
gameProfile,
mcConnection,
inboundConnection.getVirtualHost().orElse(null),
onlineMode,
inboundConnection.getIdentifiedKey()
);
} catch (Throwable throwable) {
// This should not happen
fallback.getLogger().error("Error processing {}: {}", gameProfile.getName(), throwable);
mcConnection.close(true);
return;
}
// Create an instance for the Fallback connection
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
);
// Disconnect if the protocol version could not be resolved
if (fallbackPlayer.getProtocolVersion().isUnknown()) {
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getInvalidProtocol());
return;
}
// Check if the player is already connected to the proxy but still tries to verify
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getAlreadyConnected());
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), fallbackPlayer));
// Mark the player as connected → verifying players
fallback.getConnected().put(gameProfile.getName(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a weird issue with TCPShield and other reverse proxies
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion());
fallbackPlayer.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
fallbackPlayer.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(fallbackPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(fallbackPlayer,
new FallbackVerificationHandler(fallbackPlayer, gameProfile.getName(), connectedPlayer.getUniqueId())
));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
@@ -250,27 +345,22 @@ public final class FallbackListener {
public void handle(final @NotNull LoginEvent event) {
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
LOCKDOWN_DISCONNECT, connectedPlayer.getProtocolVersion()
));
Sonar.get().getConfig().getLockdown().getDisconnect(), connectedPlayer.getProtocolVersion()));
if (Sonar.get().getConfig().LOCKDOWN_LOG_ATTEMPTS) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().LOCKDOWN_CONSOLE_LOG
.replace("%player%", event.getPlayer().getUsername())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol()))
);
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
.replace("%player%", event.getPlayer().getUsername())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
}
return;
} else if (Sonar.get().getConfig().LOCKDOWN_ENABLE_NOTIFY) {
event.getPlayer().sendMessage(
Component.text(Sonar.get().getConfig().LOCKDOWN_NOTIFICATION)
);
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
event.getPlayer().sendMessage(Component.text(Sonar.get().getConfig().getLockdown().getNotification()));
}
}
@@ -278,7 +368,7 @@ public final class FallbackListener {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int maxOnlinePerIp = Sonar.get().getConfig().MAXIMUM_ONLINE_PER_IP;
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
if (maxOnlinePerIp > 0) {
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
@@ -288,7 +378,7 @@ public final class FallbackListener {
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
TOO_MANY_ONLINE_PER_IP, connectedPlayer.getProtocolVersion()
Sonar.get().getConfig().getTooManyOnlinePerIp(), connectedPlayer.getProtocolVersion()
));
}
}
@@ -1,172 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.client.LoginInboundConnection;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
@Getter
public final class FallbackSessionHandler implements MinecraftSessionHandler {
private final Fallback fallback;
private final MinecraftConnection mcConnection;
private final LoginInboundConnection inboundConnection;
private static final MethodHandle CONNECTED_PLAYER;
static {
try {
CONNECTED_PLAYER = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
.findConstructor(ConnectedPlayer.class,
MethodType.methodType(
void.class,
VelocityServer.class,
GameProfile.class,
MinecraftConnection.class,
InetSocketAddress.class,
boolean.class,
IdentifiedKey.class
)
);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
FallbackSessionHandler(final Fallback fallback,
final @NotNull MinecraftConnection mcConnection,
final LoginInboundConnection inboundConnection,
final GameProfile gameProfile,
final InetAddress inetAddress,
final boolean onlineMode) {
this.fallback = fallback;
this.mcConnection = mcConnection;
this.inboundConnection = inboundConnection;
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Create an instance for the connected player
final ConnectedPlayer connectedPlayer;
try {
connectedPlayer = (ConnectedPlayer) CONNECTED_PLAYER.invokeExact(
mcConnection.server,
gameProfile,
mcConnection,
inboundConnection.getVirtualHost().orElse(null),
onlineMode,
inboundConnection.getIdentifiedKey()
);
} catch (Throwable throwable) {
// This should not happen
fallback.getLogger().error("Error processing {}: {}", gameProfile.getName(), throwable);
mcConnection.close(true);
return;
}
// Create an instance for the Fallback connection
final FallbackPlayerWrapper fallbackPlayer = new FallbackPlayerWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
);
// Check if the player is already connected to the proxy
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
fallbackPlayer.disconnect("Could not find any available servers.");
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().LOG_CONNECTIONS) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().LOG_DURING_ATTACK) {
fallback.getLogger().info(Sonar.get().getConfig().VERIFICATION_CONNECT_LOG
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
}
}
// Mark the player as connected → verifying players
fallback.getConnected().put(gameProfile.getName(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a weird issue with TCPShield and other reverse proxies
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_ENCODER,
FALLBACK_PACKET_ENCODER,
new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion())
);
// Replace normal decoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_DECODER,
FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(
fallbackPlayer,
new FallbackVerificationHandler(
fallbackPlayer,
gameProfile.getName(),
connectedPlayer.getUniqueId()
)
)
);
}
@Override
public void handleUnknown(final ByteBuf buf) {
mcConnection.close(true);
}
}
@@ -19,25 +19,26 @@ package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@Getter
@ToString(of = {"protocolVersion"})
@EqualsAndHashCode
@RequiredArgsConstructor
public final class FallbackPlayerWrapper implements FallbackPlayer<ConnectedPlayer, MinecraftConnection> {
public final class FallbackUserWrapper implements FallbackUser<ConnectedPlayer, MinecraftConnection> {
private final Fallback fallback;
private final ConnectedPlayer player;
private final MinecraftConnection connection;
@@ -47,14 +48,17 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ConnectedPlay
private final ProtocolVersion protocolVersion;
@Override
public void disconnect(final @NotNull String reason) {
final String serialized = ProtocolUtils.getJsonChatSerializer(connection.getProtocolVersion())
.serialize(Component.text(reason));
connection.closeWith(Disconnect.create(serialized));
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
public void write(final @NotNull Object packet) {
connection.write(packet);
}
@Override
public void delayedWrite(final @NotNull Object packet) {
connection.delayedWrite(packet);
}
}
@@ -27,9 +27,7 @@ public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
@Override
public void broadcast(final String message) {
final Component component = Component.text(message);
public void broadcast(final Component component) {
synchronized (subscribers) {
for (final String subscriber : subscribers) {
server.getPlayer(subscriber).ifPresent(player -> {
@@ -0,0 +1,13 @@
{
"id": "sonar",
"name": "$name",
"version": "$version",
"description": "$description",
"url": "$url",
"authors": [
"Jones Development",
"Sonar Contributors"
],
"dependencies": [],
"main": "$main"
}