Compare commits

..
34 Commits
Author SHA1 Message Date
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
67 changed files with 1503 additions and 1258 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.
+12 -9
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,22 @@ 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
// MiniMessage
implementation("net.kyori:adventure-text-minimessage:4.14.0")
implementation("net.kyori:adventure-text-serializer-gson:4.14.0")
implementation("net.kyori:adventure-text-serializer-legacy:4.14.0")
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.99.Final") // netty
}
}
@@ -62,6 +64,7 @@ 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"
}
}
+2
View File
@@ -0,0 +1,2 @@
group=xyz.jonesdev.sonar
version=2.0.2
+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
@@ -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,11 +54,21 @@ public interface Sonar {
@NotNull Verbose getVerboseHandler();
/**
* Set a custom verbose handler
*/
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
* Reloads the entire plugin
*/
void reload();
@NotNull
default SonarEventManager getEventManager() {
return SonarEventManager.INSTANCE;
}
@NotNull
default SonarVersion getVersion() {
return SonarVersion.GET;
@@ -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.serializer.legacy.LegacyComponentSerializer;
@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(LegacyComponentSerializer.legacyAmpersand().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final Component component) {
audience.sendMessage(component);
}
}
@@ -0,0 +1,85 @@
/*
* 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(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.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(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.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 = component.clickEvent(
net.kyori.adventure.text.event.ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " ")
).hoverEvent(
net.kyori.adventure.text.event.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);
});
}
}
@@ -21,7 +21,7 @@ import lombok.Getter;
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.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
@@ -49,11 +49,9 @@ 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 final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", info.name() + " (" + arguments + ")"));
}
public abstract void execute(final @NotNull CommandInvocation invocation);
@@ -27,8 +27,11 @@ 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(
@@ -48,6 +51,7 @@ public final class SimpleYamlConfig {
throw new IllegalStateException("Could not create folder?!");
}
this.file = file;
this.yaml = new YamlFile(file.getPath());
}
@@ -59,16 +63,20 @@ 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.PRETTY);
yaml.options().headerFormatter().commentPrefix("# ");
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
@@ -48,10 +48,8 @@ public final class VerifiedPlayerController {
private final @NotNull SonarConfiguration.DatabaseType 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().getDatabaseType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
@@ -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.getDatabaseType().getDependencies().length];
for (final Dependency dependency : config.getDatabaseType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
@@ -50,9 +50,9 @@ public class DependencyLoader {
urls[dependency.ordinal()] = url;
}
final String type = config.DATABASE_TYPE.name().toLowerCase();
final String type = config.getDatabaseType().name().toLowerCase();
final String databaseURL =
"jdbc:" + type + "://" + config.SQL_URL + ":" + config.SQL_PORT + "/" + config.SQL_DATABASE;
"jdbc:" + type + "://" + config.getSqlUrl() + ":" + config.getSqlPort() + "/" + config.getSqlDatabase();
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config);
@@ -64,17 +64,17 @@ public class DependencyLoader {
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 Class<?> driverClass = classLoader.loadClass(configuration.getDatabaseType().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.getSqlUser().isEmpty()) {
properties.put("user", configuration.getSqlUser());
}
if (!configuration.SQL_PASSWORD.isEmpty()) {
properties.put("password", configuration.SQL_PASSWORD);
if (!configuration.getSqlPassword().isEmpty()) {
properties.put("password", configuration.getSqlPassword());
}
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;
}
@@ -65,7 +65,7 @@ public final class Fallback {
};
public boolean isPotentiallyUnderAttack() {
return connected.size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK
|| queue.getQueuedPlayers().size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK;
return connected.size() > SONAR.getConfig().getMinimumPlayersForAttack()
|| queue.getQueuedPlayers().size() > SONAR.getConfig().getMinimumPlayersForAttack();
}
}
@@ -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().getMaximumQueuePolls();
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
@@ -72,10 +86,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().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_FAILED_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
@@ -89,8 +106,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().getVerificationBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
@@ -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);
}
}
@@ -47,7 +47,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 +57,8 @@ public abstract class Verbose implements JVMProfiler {
broadcast(prepareActionBarFormat());
}
protected final @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().ACTION_BAR_LAYOUT
protected @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().getActionBarLayout()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
@@ -82,33 +82,35 @@ public abstract class Verbose implements JVMProfiler {
}
protected final String nextAnimation() {
val animations = Sonar.get().getConfig().ANIMATION;
val animations = Sonar.get().getConfig().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 void broadcast(final String message) {
// This should be replaced by the wrapper
}
/**
* @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,
@@ -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
@@ -103,6 +46,11 @@ public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBa
final String label,
final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.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,14 +60,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);
sender.sendMessage(Sonar.get().getConfig().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
Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
);
return false;
@@ -130,18 +78,7 @@ 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);
}
};
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
@@ -156,10 +93,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().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
@@ -167,24 +102,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().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().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().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -197,12 +130,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 +142,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()) {
+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()));
@@ -136,8 +117,5 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@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,11 +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;
@@ -30,8 +27,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 +39,21 @@ 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) {
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(new TextComponent(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 +63,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));
sender.sendMessage(new TextComponent(Sonar.get().getConfig().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))
));
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
return;
}
@@ -135,18 +79,7 @@ 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));
}
};
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
@@ -161,8 +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
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission())
);
return;
@@ -172,24 +105,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().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().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().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -202,14 +133,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 (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
event.getPlayer().disconnect(LOCKDOWN_DISCONNECT);
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().getLockdownDisconnect();
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().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
.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().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdownNotification()));
}
}
@@ -22,11 +22,11 @@ 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;
@@ -35,7 +35,7 @@ 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
@@ -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
@@ -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;
@@ -41,6 +42,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 +65,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,10 +86,11 @@ 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();
}
}
@@ -92,7 +102,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().getDatabaseType()) {
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
}
@@ -102,7 +112,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Update ratelimiter
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
getConfig().VERIFICATION_DELAY, TimeUnit.MILLISECONDS, 250L
getConfig().getVerificationDelay(), TimeUnit.MILLISECONDS, 250L
);
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
@@ -24,7 +24,8 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
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;
@@ -41,7 +42,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
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;
@@ -72,10 +73,10 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private final boolean canMove;
}
public FallbackVerificationHandler(final @NotNull FallbackPlayer<?, ?> player,
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
final @NotNull String username,
final @NotNull UUID uuid) {
this.player = player;
this.user = user;
this.username = username;
this.uuid = uuid;
this.transactionId = (short) RANDOM.nextInt();
@@ -83,14 +84,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
this.state = State.KEEP_ALIVE;
// Send LoginSuccess packet to make the client think they are joining the server
player.write(new ServerLoginSuccess(username, uuid));
user.write(new ServerLoginSuccess(username, uuid));
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Send first KeepAlive to check if the connection is somewhat responsive
player.write(new KeepAlive(verifyKeepAliveId));
user.write(new KeepAlive(verifyKeepAliveId));
}
}
@@ -99,7 +100,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// and go on with the flow of the verification.
state = State.TRANSACTION;
// Send a transaction with a
player.write(new Transaction(
user.write(new Transaction(
0, transactionId, false
));
}
@@ -109,7 +110,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// 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()));
}
private void sendChunkData() {
@@ -117,38 +118,36 @@ 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);
user.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.write(UPDATE_SECTION_BLOCKS);
}
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().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 {
// 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);
// TODO: fix this?
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) return true;
// No need to check for empty or too long client brands since
// ProtocolUtil#readString already does exactly that.
final String read = ProtocolUtil.readString(content, Sonar.get().getConfig().getMaximumBrandLength());
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().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;
}
@@ -157,24 +156,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
@Override
public void handle(final @NotNull FallbackPacket packet) {
// 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;
final int maxPackets = Sonar.get().getConfig().getMaximumLoginPackets()
+ Sonar.get().getConfig().getMaximumMovementTicks();
checkFrame(++receivedPackets < 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().getVerificationTimeout();
// 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 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");
// The correct KeepAlive packet has been received
@@ -185,7 +187,7 @@ 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) {
@@ -203,11 +205,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// 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) {
@@ -229,18 +231,18 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
checkFrame(transaction.getId() == transactionId, "invalid transaction id");
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().CHECK_GRAVITY) {
if (!Sonar.get().getConfig().isCheckGravity()) {
finish();
return;
}
// Make sure the player is unable to fly (the player is in spectator mode)
player.write(DEFAULT_ABILITIES);
user.write(DEFAULT_ABILITIES);
// Teleport the player to the spawn position
player.write(SPAWN_TELEPORT);
user.write(SPAWN_TELEPORT);
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleports
sendChunkData();
} else {
@@ -304,12 +306,11 @@ 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
// Check if the chunk might be unloaded
if (deltaY > 0.07) {
// Verify the player if they sent correct movement packets
if (movementTick++ >= MAX_MOVEMENT_TICK) {
if (Sonar.get().getConfig().CHECK_COLLISIONS) {
if (Sonar.get().getConfig().isCheckCollisions()) {
if (state != State.COLLISIONS) {
// Set the state to COLLISIONS to avoid false positives
// and go on with the flow of the verification.
@@ -334,26 +335,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Checking collisions is disabled, just finish verification
finish();
}
} else if (y >= DEFAULT_Y_COLLIDE_POSITION && y <= DYNAMIC_SPAWN_Y_POSITION) {
} else if (y <= DYNAMIC_SPAWN_Y_POSITION && y >= DEFAULT_Y_COLLIDE_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);
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.01, "invalid offset: " + y + ", " + offsetY);
final String verbose = String.format("%d: %.7f %.10f %.10f!%.10f", movementTick, y, offsetY, deltaY, predictedY);
checkFrame(offsetY < 0.01, verbose);
}
}
}
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 +363,20 @@ 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(), uuid, 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, uuid, 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().getVerificationSuccess());
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerificationSuccessfulLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
}
@@ -23,21 +23,21 @@ 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.common.utility.protocol.VarIntUtil.readVarInt;
@RequiredArgsConstructor
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackPlayer<?, ?> connection;
private final FallbackUser<?, ?> user;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private final FallbackPacketListener listener;
public FallbackPacketDecoder(final @NotNull FallbackPlayer<?, ?> connection,
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
final @NotNull FallbackPacketListener listener) {
this.connection = connection;
this.user = user;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.SERVERBOUND, connection.getProtocolVersion()
FallbackPacketRegistry.Direction.SERVERBOUND, user.getProtocolVersion()
);
this.listener = listener;
}
@@ -64,14 +64,14 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
doLengthSanityChecks(byteBuf, packet);
try {
packet.decode(byteBuf, connection.getProtocolVersion());
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
connection.fail("could not decode packet");
user.fail("could not decode packet");
throw new CorruptedFrameException("Failed to decode packet");
}
if (byteBuf.isReadable()) {
connection.fail("could not read packet to end");
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
@@ -85,15 +85,15 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
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");
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");
throw new CorruptedFrameException("Packet too small");
}
}
@@ -123,7 +123,7 @@ public enum FallbackPacketRegistry {
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
map(34, MINECRAFT_1_7_2, true),
map(0x22, MINECRAFT_1_7_2, true),
map(16, MINECRAFT_1_9, true),
map(15, MINECRAFT_1_13, true),
map(16, MINECRAFT_1_15, true),
@@ -99,7 +99,7 @@ public class FallbackPreparer {
JOIN_GAME_1_19_4 = createJoinGamePacket(MINECRAFT_1_19_4);
JOIN_GAME_1_20 = createJoinGamePacket(MINECRAFT_1_20);
MAX_MOVEMENT_TICK = Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
MAX_MOVEMENT_TICK = Sonar.get().getConfig().getMaximumMovementTicks();
PREPARED_MOVEMENT_PACKETS = new double[MAX_MOVEMENT_TICK + 1];
for (int i = 0; i < MAX_MOVEMENT_TICK + 1; i++) {
@@ -122,7 +122,7 @@ public class FallbackPreparer {
8, DYNAMIC_SPAWN_Y_POSITION, 8,
0f, 0f,
RANDOM.nextInt(Short.MAX_VALUE),
true
false // 1.7 support
);
// Prepare collision platform positions
@@ -184,7 +184,7 @@ public class FallbackPreparer {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setGamemode(Sonar.get().getConfig().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
return joinGame;
@@ -196,7 +196,7 @@ public class FallbackPreparer {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setGamemode(Sonar.get().getConfig().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
joinGame.setDimensionInfo(DIMENSION);
@@ -38,14 +38,20 @@ public final class DefaultSpawnPosition implements FallbackPacket {
@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);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeInt(x);
byteBuf.writeInt(y);
byteBuf.writeInt(z);
} else {
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);
byteBuf.writeLong(encoded);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
byteBuf.writeFloat(angle);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
byteBuf.writeFloat(angle);
}
}
}
@@ -22,6 +22,8 @@ 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.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -48,7 +50,7 @@ public final class Disconnect implements FallbackPacket {
writeString(byteBuf, Objects.requireNonNull(reason));
}
public static Disconnect create(final String serialized) {
return new Disconnect(serialized);
public static Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
}
}
@@ -48,10 +48,20 @@ public final class EmptyChunkData implements FallbackPacket {
private static final byte[] LEGACY_FILLER_BYTES = new byte[256];
private static final byte[] MODERN_FILLER_BYTES = new byte[1024];
private static final long[] MASK;
// Prepare nbt for 1.18 and pre-1.18
private static final CompoundBinaryTag MODERN_TAG, LEGACY_TAG;
static {
final BitSet bitSet = new BitSet();
for (int i = 0; i < 16; i++) {
bitSet.set(i, false);
}
MASK = bitSet.toLongArray();
MODERN_TAG = prepareNBT(false);
LEGACY_TAG = prepareNBT(true);
}
@@ -73,32 +83,26 @@ public final class EmptyChunkData implements FallbackPacket {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, MASK.length);
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0 && protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true);
}
for (final long l : MASK) {
byteBuf.writeLong(l);
}
}
} else {
byteBuf.writeBoolean(true); // Full chunk
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) == 0) {
byteBuf.writeShort(1);
} else {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // Ignore old data
}
if (protocolVersion.compareTo(ProtocolVersion.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 +125,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(new byte[2]); // 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) {
@@ -36,6 +36,10 @@ public final class Position implements FallbackPacket {
@Override
public void decode(final ByteBuf byteBuf, final 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();
@@ -45,4 +49,14 @@ public final class Position implements FallbackPacket {
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
}
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 25;
}
}
@@ -41,6 +41,10 @@ public final class PositionLook implements FallbackPacket {
@Override
public void decode(final ByteBuf byteBuf, final 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();
@@ -55,14 +59,30 @@ public final class PositionLook implements FallbackPacket {
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;
}
}
@@ -41,7 +41,7 @@ public final class Transaction implements FallbackPacket {
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);
@@ -26,15 +26,14 @@ 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;
@@ -44,11 +43,22 @@ public final class UpdateSectionBlocks implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeInt(sectionX);
byteBuf.writeInt(sectionZ);
writeVarInt(byteBuf, changedBlocks.length);
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
writeVarInt(byteBuf, block.getType().getId(protocolVersion));
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort((short) changedBlocks.length);
byteBuf.writeInt(4 * changedBlocks.length);
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
byteBuf.writeShort(block.getType().getId(protocolVersion));
}
} else {
writeVarInt(byteBuf, changedBlocks.length);
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
writeVarInt(byteBuf, block.getType().getId(protocolVersion));
}
}
} else {
final ChangedBlock lastChangedBlock = changedBlocks[changedBlocks.length - 1];
@@ -44,15 +44,14 @@ 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().getIncorrectCommandUsage()
.replace("%usage%", "blacklist add <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
@@ -61,41 +60,40 @@ public final class BlacklistCommand extends Subcommand {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_DUPLICATE);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistDuplicate());
return;
}
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD_WARNING
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD
invocation.getSender().sendMessage(SONAR.getConfig().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().getIncorrectCommandUsage()
.replace("%usage%", "blacklist remove <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
@@ -104,21 +102,21 @@ public final class BlacklistCommand extends Subcommand {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistNotFound());
return;
}
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_REMOVE
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -127,21 +125,18 @@ 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().getBlacklistEmpty());
return;
}
SONAR.getFallback().getBlacklisted().invalidateAll();
invocation.getSender().sendMessage(
SONAR.getConfig().BLACKLIST_CLEARED
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted))
);
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
break;
}
@@ -30,14 +30,12 @@ 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().isLockdownEnabled();
SONAR.getConfig().setLockdownEnabled(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().getLockdownActivated() : SONAR.getConfig().getLockdownDeactivated());
}
}
@@ -21,6 +21,7 @@ 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",
@@ -30,15 +31,12 @@ 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().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().getReloaded()
.replace("%taken%", String.valueOf(timer.delay())));
}
}
@@ -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().getVerboseUnsubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_UNSUBSCRIBED);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseUnsubscribed());
return;
}
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_SUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseSubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_SUBSCRIBED);
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
}
}
@@ -46,27 +46,26 @@ public final class VerifiedCommand extends Subcommand {
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().getIncorrectCommandUsage()
.replace("%usage%", "verified history <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
final String realInetAddress = "/" + rawInetAddress;
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedNotFound());
return;
}
invocation.getSender().sendMessage("§ePrevious UUIDs for " + inetAddress + ":");
invocation.getSender().sendMessage("§ePrevious UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(realInetAddress)) {
invocation.getSender().sendMessage(" §7▪ §f" + uuid.toString());
@@ -76,28 +75,27 @@ public final class VerifiedCommand extends Subcommand {
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", "verified remove <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
final String realInetAddress = "/" + rawInetAddress;
if (LOCK.contains(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_BLOCKED);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedBlocked());
return;
}
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedNotFound());
return;
}
@@ -106,8 +104,8 @@ public final class VerifiedCommand extends Subcommand {
SONAR.getVerifiedPlayerController().remove(realInetAddress);
LOCK.remove(realInetAddress);
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_REMOVE
.replace("%ip%", inetAddress));
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -115,19 +113,18 @@ 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().getVerifiedEmpty());
return;
}
SONAR.getVerifiedPlayerController().clearAll();
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_CLEARED
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
break;
}
@@ -53,34 +53,21 @@ public class ProtocolUtil {
public static @NotNull String readString(final ByteBuf buf,
final int cap) throws CorruptedFrameException {
return readString(buf, cap, false);
}
public static @NotNull String readString(final ByteBuf buf,
final int cap,
final boolean legacy) throws CorruptedFrameException {
final int length = VarIntUtil.readVarInt(buf);
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(buf.isReadable(length), "Got an invalid string length");
final String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.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()));
@@ -129,8 +113,5 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
@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,19 @@ 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) {
if (!(invocation.source() instanceof ConsoleCommandSource)) {
// Check if the player actually has the permission to run the command
if (!invocation.source().hasPermission("sonar.command")) {
invocation.source().sendMessage(Component.text(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 +58,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));
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().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))
));
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
return;
}
@@ -114,20 +74,7 @@ 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));
}
};
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (invocation.arguments().length > 0) {
// Search subcommand if command arguments are present
@@ -142,8 +89,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
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission())
);
return;
@@ -153,25 +100,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().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(invocation.source() instanceof ConsoleCommandSource)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
invocationSource.sendMessage(Sonar.get().getConfig().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().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
@@ -184,14 +129,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 +170,4 @@ public final class SonarCommand implements SimpleCommand, SonarBaseCommand {
}
return emptyList();
}
@Override
public boolean hasPermission(final @NotNull Invocation invocation) {
return invocation.source().hasPermission("sonar.command");
}
}
@@ -55,34 +55,11 @@ 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.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();
@@ -133,14 +110,14 @@ public final class FallbackListener {
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(mcConnection.getSessionHandler());
initialConnection.getConnection().closeWith(Disconnect.create(
BLACKLISTED,
Sonar.get().getConfig().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().ENABLE_VERIFICATION) return;
if (!Sonar.get().getConfig().isEnableVerification()) return;
// Check if the player is already verified.
// No one wants to be verified over and over again.
@@ -162,7 +139,7 @@ public final class FallbackListener {
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
ALREADY_VERIFYING,
Sonar.get().getConfig().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
@@ -170,9 +147,9 @@ public final class FallbackListener {
// 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) {
if (fallback.getConnected().size() > Sonar.get().getConfig().getMaximumVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
TOO_MANY_PLAYERS,
Sonar.get().getConfig().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
@@ -181,7 +158,7 @@ public final class FallbackListener {
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
TOO_FAST_RECONNECT,
Sonar.get().getConfig().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
return;
@@ -190,7 +167,7 @@ public final class FallbackListener {
// 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,
Sonar.get().getConfig().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
@@ -214,9 +191,8 @@ public final class FallbackListener {
// 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()));
if (!Sonar.get().getConfig().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getInvalidUsername(), mcConnection.getProtocolVersion()));
return;
}
@@ -225,10 +201,7 @@ public final class FallbackListener {
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(
Sonar.get().getConfig().VERIFICATION_READ_TIMEOUT,
TimeUnit.MILLISECONDS
)
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerificationReadTimeout(), TimeUnit.MILLISECONDS)
);
// We need to determine if the player is premium before we handle the connection,
@@ -250,27 +223,22 @@ public final class FallbackListener {
public void handle(final @NotNull LoginEvent event) {
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
if (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
LOCKDOWN_DISCONNECT, connectedPlayer.getProtocolVersion()
));
Sonar.get().getConfig().getLockdownDisconnect(), 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().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
.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().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(Component.text(Sonar.get().getConfig().getLockdownNotification()));
}
}
@@ -278,7 +246,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().getMaximumOnlinePerIp();
if (maxOnlinePerIp > 0) {
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
@@ -288,7 +256,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()
));
}
}
@@ -29,6 +29,7 @@ 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.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;
@@ -106,7 +107,7 @@ public final class FallbackSessionHandler implements MinecraftSessionHandler {
}
// Create an instance for the Fallback connection
final FallbackPlayerWrapper fallbackPlayer = new FallbackPlayerWrapper(
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
@@ -121,17 +122,20 @@ public final class FallbackSessionHandler implements MinecraftSessionHandler {
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().LOG_CONNECTIONS) {
if (Sonar.get().getConfig().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().LOG_DURING_ATTACK) {
fallback.getLogger().info(Sonar.get().getConfig().VERIFICATION_CONNECT_LOG
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerificationConnectLog()
.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);
@@ -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 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,10 +48,8 @@ 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
@@ -0,0 +1,13 @@
{
"id": "sonar",
"name": "$name",
"version": "$version",
"description": "$description",
"url": "$url",
"authors": [
"Jones Development",
"Sonar Contributors"
],
"dependencies": [],
"main": "$main"
}