mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 17:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9c212b9e4 | ||
|
|
42da141f2b | ||
|
|
e0bc83f8ef | ||
|
|
59822ef541 | ||
|
|
38fbfff896 | ||
|
|
6725975cf1 | ||
|
|
d59fd4b431 | ||
|
|
a76ca2f91d | ||
|
|
30803ba6e4 | ||
|
|
194232804b | ||
|
|
19c97b26b6 | ||
|
|
d66719b3e0 | ||
|
|
9226908002 | ||
|
|
ac74f14dc0 | ||
|
|
8f11a53b38 | ||
|
|
0b4394624f | ||
|
|
d8f7e288e8 | ||
|
|
3f3411ca9d | ||
|
|
14ae85384a | ||
|
|
8204477eb6 | ||
|
|
44754e5027 | ||
|
|
847936ed08 | ||
|
|
aa90a1fa22 | ||
|
|
9ac8447226 | ||
|
|
6b6acb8371 | ||
|
|
f3af903114 | ||
|
|
67bd3786d7 | ||
|
|
2b379b0dfa | ||
|
|
b81cc0e9f5 | ||
|
|
0efc6ae157 | ||
|
|
4ae14c58a6 | ||
|
|
3bb752f435 | ||
|
|
eeb9a446fb | ||
|
|
dfe728f7a9 | ||
|
|
3945e813dc | ||
|
|
5068ebcd91 | ||
|
|
f20d162dcc | ||
|
|
acc9df0b8d | ||
|
|
3636674eff | ||
|
|
d2b3b0adae | ||
|
|
194c7645ed | ||
|
|
860594400a | ||
|
|
20f5b135aa | ||
|
|
ef31dd4c87 | ||
|
|
8361f1990d | ||
|
|
bf9e77a437 |
No files matched your search
+4
-4
@@ -9,6 +9,7 @@
|
||||
|
||||
<!-- Badges & icons -->
|
||||
[](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
|
||||
[](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
|
||||
[](https://github.com/jonesdevelopment/sonar/releases)
|
||||
[](https://github.com/jonesdevelopment/sonar/issues)
|
||||
[](https://jonesdev.xyz/discord)
|
||||
@@ -42,8 +43,8 @@
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
|
||||
all types of bots.
|
||||
- Spigot & BungeeCord
|
||||
- The compression method is changed
|
||||
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
|
||||
- Duplicate login/status packet spam fix
|
||||
- Run server ping handling asynchronously
|
||||
- The Varint decoder is updated to Velocity's
|
||||
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
|
||||
|
||||
@@ -90,5 +91,4 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
|
||||
## Credits
|
||||
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
|
||||
- The nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
|
||||
- The compression and Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
+15
-6
@@ -22,12 +22,9 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
maven(url = "https://jitpack.io") // simple-yaml
|
||||
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java")
|
||||
apply(plugin = "com.github.johnrengelman.shadow")
|
||||
|
||||
@@ -49,7 +46,7 @@ subprojects {
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
|
||||
compileOnly("io.netty:netty-all:4.1.101.Final") // netty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,16 +71,28 @@ tasks {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("Sonar.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception
|
||||
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
|
||||
relocate("org.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.velocitypowered.natives", "xyz.jonesdev.sonar.libs.natives")
|
||||
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
|
||||
exclude("net.kyori.adventure.text.**")
|
||||
exclude("net.kyori.adventure.audience.**")
|
||||
}
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/versions/**")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.5
|
||||
version=2.0.10
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
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;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
@@ -34,10 +36,14 @@ public interface Sonar {
|
||||
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
|
||||
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
|
||||
|
||||
Gson GENERAL_GSON = new GsonBuilder()
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the server
|
||||
* @return The platform the plugin is being run on
|
||||
*/
|
||||
@NotNull ServerWrapper getServer();
|
||||
@NotNull SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the plugin logger so we can use the logger everywhere
|
||||
@@ -76,6 +82,12 @@ public interface Sonar {
|
||||
return SonarEventManager.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default AttackTracker getAttackTracker() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
return AttackTracker.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Fallback getFallback() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.attack;
|
||||
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackDetectedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackMitigatedEvent;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AttackTracker implements JVMProfiler {
|
||||
public static final AttackTracker INSTANCE = new AttackTracker();
|
||||
private @Nullable AttackStatistics currentAttack;
|
||||
private int attackThreshold;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public static final class AttackStatistics {
|
||||
private final SystemTimer duration = new SystemTimer();
|
||||
private final SystemTimer timer = new SystemTimer();
|
||||
private int peakJoinsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
private int successfulVerifications, failedVerifications;
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Sonar.get().getVerboseHandler().getLoginsPerSecond().estimatedSize();
|
||||
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
|
||||
|
||||
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
|
||||
|| verifyingPlayers > minPlayers // Check the number of verifying players.
|
||||
|| queuedPlayers > minPlayers) { // Check the number of queued players.
|
||||
// Increment attack threshold
|
||||
++attackThreshold;
|
||||
|
||||
// An attack has been detected
|
||||
if (currentAttack == null) {
|
||||
currentAttack = new AttackStatistics();
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
}
|
||||
|
||||
// Update attack statistics
|
||||
if (joinsPerSecond > currentAttack.peakJoinsPerSecond) {
|
||||
// Update joins per second peak if necessary
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final double processCPUUsage = getProcessCPUUsage();
|
||||
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
|
||||
// Update cpu usage peak if necessary
|
||||
currentAttack.peakProcessCPUUsage = processCPUUsage;
|
||||
}
|
||||
final long processMemoryUsage = getUsedMemory();
|
||||
if (processMemoryUsage > currentAttack.peakProcessMemoryUsage) {
|
||||
// Update memory consumption peak if necessary
|
||||
currentAttack.peakProcessMemoryUsage = processMemoryUsage;
|
||||
}
|
||||
} else if (currentAttack != null) {
|
||||
if (currentAttack.duration.delay() > Sonar.get().getConfig().getMinAttackDuration()
|
||||
&& currentAttack.timer.delay() > Sonar.get().getConfig().getAttackCooldownDelay()) {
|
||||
// The current attack has stopped
|
||||
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
|
||||
|
||||
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
|
||||
// Save attack statistics
|
||||
final long deltaInMillis = currentAttack.duration.delay();
|
||||
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
|
||||
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
|
||||
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
|
||||
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
|
||||
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
|
||||
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
|
||||
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklisted().estimatedSize();
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
|
||||
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
}
|
||||
|
||||
// Reset the attack status
|
||||
currentAttack = null;
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
} else {
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isCurrentlyUnderAttack() {
|
||||
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
|
||||
return currentAttack != null && currentAttack.timer.delay() < minDelay;
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,14 @@ package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = "rawArguments")
|
||||
public final class CommandInvocation {
|
||||
private final InvocationSource sender;
|
||||
private final Subcommand command;
|
||||
private final Subcommand subcommand;
|
||||
private final String[] rawArguments;
|
||||
}
|
||||
@@ -23,11 +23,15 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class InvocationSource {
|
||||
private final String name;
|
||||
private final Audience audience;
|
||||
private final boolean player;
|
||||
private final Predicate<String> permissionFunction;
|
||||
|
||||
/**
|
||||
* Sends an empty chat message to the command executor
|
||||
|
||||
@@ -18,14 +18,18 @@
|
||||
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 net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public interface SonarCommand {
|
||||
List<String> TAB_SUGGESTIONS = new ArrayList<>();
|
||||
@@ -34,63 +38,114 @@ public interface SonarCommand {
|
||||
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
|
||||
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new Vector<>();
|
||||
|
||||
default void cacheHelpMessage() {
|
||||
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
|
||||
Component.text("Running Sonar " + Sonar.get().getVersion()
|
||||
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
|
||||
+ ".", NamedTextColor.YELLOW),
|
||||
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
|
||||
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
|
||||
Component.empty(),
|
||||
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
|
||||
Component.textOfChildren(
|
||||
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
|
||||
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
|
||||
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
|
||||
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
|
||||
"/issues"))
|
||||
),
|
||||
Component.empty()
|
||||
));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
|
||||
Component component = Component.textOfChildren(
|
||||
Component.text(" ▪ ", NamedTextColor.GRAY),
|
||||
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
|
||||
Component.text(" - ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
|
||||
);
|
||||
|
||||
Component hoverComponent = Component.textOfChildren(
|
||||
Component.text("Only players: ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getInfo().onlyPlayers() ? "✔" : "✗",
|
||||
sub.getInfo().onlyPlayers() ? NamedTextColor.GREEN : NamedTextColor.RED),
|
||||
Component.newline(),
|
||||
Component.text("Require console: ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getInfo().onlyConsole() ? "✔" : "✗",
|
||||
sub.getInfo().onlyConsole() ? NamedTextColor.GREEN : NamedTextColor.RED),
|
||||
Component.newline(),
|
||||
Component.text("Permission: ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getPermission(), NamedTextColor.WHITE)
|
||||
);
|
||||
if (sub.getInfo().aliases().length > 0) {
|
||||
hoverComponent = hoverComponent
|
||||
.append(Component.newline())
|
||||
.append(Component.text("Aliases: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(sub.getAliases(), NamedTextColor.WHITE));
|
||||
default void handle(final @NotNull InvocationSource source, final String[] args) {
|
||||
if (source.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
component = component
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
|
||||
"/sonar " + sub.getInfo().name() + " "))
|
||||
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, hoverComponent));
|
||||
CACHED_HELP_MESSAGE.add(component);
|
||||
// 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
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// 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) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(source);
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
subcommand.get().invoke(source, args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Component component : CACHED_HELP_MESSAGE) {
|
||||
source.sendMessage(component);
|
||||
}
|
||||
}
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// Cache help message
|
||||
CACHED_HELP_MESSAGE.clear();
|
||||
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
|
||||
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
|
||||
.replace("%version%", Sonar.get().getVersion().getFormatted())
|
||||
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
|
||||
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
}
|
||||
|
||||
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
|
||||
.replace("%subcommand%", subcommand.getInfo().name())
|
||||
.replace("%description%", subcommand.getInfo().description())
|
||||
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%permission%", subcommand.getPermission())
|
||||
.replace("%aliases%", subcommand.getAliases()));
|
||||
CACHED_HELP_MESSAGE.add(deserialized);
|
||||
});
|
||||
|
||||
// Don't re-cache tab suggestions
|
||||
if (!TAB_SUGGESTIONS.isEmpty()) return;
|
||||
// Cache tab suggestions
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
|
||||
if (arguments.length <= 1) {
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (arguments.length == 2) {
|
||||
final String subCommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@@ -51,18 +49,7 @@ public abstract class Subcommand {
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
// We perform a regex check beforehand,
|
||||
// so we don't have to create a new InetAddress,
|
||||
// therefore, potentially saving some performance.
|
||||
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
@@ -72,7 +59,7 @@ public abstract class Subcommand {
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
@@ -83,5 +70,31 @@ public abstract class Subcommand {
|
||||
.replace("%usage%", info.name() + " (" + arguments + ")"));
|
||||
}
|
||||
|
||||
public abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
|
||||
// Check if the subcommand can only be executed by players
|
||||
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the subcommand can only be executed though console
|
||||
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command
|
||||
execute(commandInvocation);
|
||||
}
|
||||
|
||||
protected abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -36,8 +37,8 @@ public final class SimpleYamlConfig {
|
||||
|
||||
private static final List<String> HEADER = Arrays.asList(
|
||||
"",
|
||||
"Running Sonar version " + Sonar.get().getVersion()
|
||||
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName(),
|
||||
String.format("Running Sonar version %s on %s",
|
||||
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
|
||||
"Need help or have questions? https://jonesdev.xyz/discord",
|
||||
"https://github.com/jonesdevelopment/sonar",
|
||||
""
|
||||
@@ -47,7 +48,7 @@ public final class SimpleYamlConfig {
|
||||
this(new File(dataFolder, fileName + ".yml"), dataFolder);
|
||||
}
|
||||
|
||||
private SimpleYamlConfig(final File file, final File folder) {
|
||||
private SimpleYamlConfig(final File file, final @NotNull File folder) {
|
||||
if (!folder.exists() && !folder.mkdir()) {
|
||||
throw new IllegalStateException("Could not create folder?!");
|
||||
}
|
||||
@@ -93,12 +94,27 @@ public final class SimpleYamlConfig {
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public Object getObject(final String path, final Object def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getString(path, def);
|
||||
return yaml.get(path, def);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path, final List<String> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,15 @@ import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.dependencies.Dependency;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
|
||||
@@ -48,9 +48,9 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
private final Verification verification = new Verification();
|
||||
@Getter
|
||||
private final Lockdown lockdown = new Lockdown();
|
||||
@Getter
|
||||
private final Database database = new Database();
|
||||
@Getter
|
||||
private final Webhook webhook = new Webhook();
|
||||
|
||||
public SonarConfiguration(final @NotNull File pluginFolder) {
|
||||
this.pluginFolder = pluginFolder;
|
||||
@@ -68,12 +68,19 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
private int minPlayersForAttack;
|
||||
@Getter
|
||||
private int minAttackDuration;
|
||||
@Getter
|
||||
private int minAttackThreshold;
|
||||
@Getter
|
||||
private int attackCooldownDelay;
|
||||
@Getter
|
||||
private Component tooManyOnlinePerIp;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private String actionBarLayout;
|
||||
private String actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@@ -86,9 +93,52 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verification {
|
||||
private boolean enabled;
|
||||
private boolean checkGravity;
|
||||
private boolean checkCollisions;
|
||||
private Timing timing;
|
||||
|
||||
@Getter
|
||||
public enum Timing {
|
||||
ALWAYS, DURING_ATTACK, NEVER
|
||||
}
|
||||
|
||||
private final Map map = new Map();
|
||||
private final Gravity gravity = new Gravity();
|
||||
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private boolean enabled;
|
||||
private boolean drawBackgroundNoise;
|
||||
private boolean randomizePositions;
|
||||
private boolean randomizeFontSize;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
private String dictionary;
|
||||
private Component enterCode;
|
||||
private Component failedCaptcha;
|
||||
private String enterCodeActionBar;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Gravity {
|
||||
private boolean enabled;
|
||||
private Gamemode gamemode;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
private Component youAreBeingChecked;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2),
|
||||
// Keep this for backwards compatibility
|
||||
SPECTATOR(2);
|
||||
|
||||
private final int id;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
@@ -99,15 +149,14 @@ public final class SonarConfiguration {
|
||||
private String failedLog;
|
||||
private String successLog;
|
||||
private String blacklistLog;
|
||||
private short gamemodeId;
|
||||
|
||||
private int maxBrandLength;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
private int maxVerifyingPlayers;
|
||||
private int maxLoginPackets;
|
||||
private int maxPing;
|
||||
private int readTimeout;
|
||||
private int reconnectDelay;
|
||||
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
|
||||
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
|
||||
|
||||
private Component tooManyPlayers;
|
||||
private Component tooFastReconnect;
|
||||
@@ -119,6 +168,7 @@ public final class SonarConfiguration {
|
||||
private Component alreadyVerifying;
|
||||
private Component alreadyQueued;
|
||||
private Component blacklisted;
|
||||
private Component protocolBlacklisted;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -127,7 +177,6 @@ public final class SonarConfiguration {
|
||||
private String incorrectCommandUsage;
|
||||
private String incorrectIpAddress;
|
||||
private String illegalIpAddress;
|
||||
private String unknownIpAddress;
|
||||
private String subCommandNoPerm;
|
||||
private String playersOnly;
|
||||
private String consoleOnly;
|
||||
@@ -157,10 +206,11 @@ public final class SonarConfiguration {
|
||||
private String networkStatistics;
|
||||
private String cpuStatistics;
|
||||
|
||||
private List<String> helpHeader;
|
||||
private String helpSubcommands;
|
||||
|
||||
private String verboseSubscribed;
|
||||
private String verboseUnsubscribed;
|
||||
private String verboseSubscribedOther;
|
||||
private String verboseUnsubscribedOther;
|
||||
|
||||
private String reloading;
|
||||
private String reloaded;
|
||||
@@ -173,11 +223,10 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Type {
|
||||
MYSQL(new Dependency[]{Dependency.MYSQL}, "com.mysql.cj.jdbc.NonRegisteringDriver"),
|
||||
MARIADB(new Dependency[]{Dependency.MYSQL, Dependency.MARIADB}, "org.mariadb.jdbc.Driver"),
|
||||
MYSQL(Dependency.MYSQL, "com.mysql.cj.jdbc.NonRegisteringDriver"),
|
||||
NONE(null, null);
|
||||
|
||||
private final Dependency[] dependencies;
|
||||
private final Dependency dependency;
|
||||
private final String driverClassName;
|
||||
}
|
||||
|
||||
@@ -191,19 +240,38 @@ public final class SonarConfiguration {
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Lockdown {
|
||||
@Setter
|
||||
private boolean enabled;
|
||||
private boolean notifyAdmins;
|
||||
private boolean logAttempts;
|
||||
private Component disconnect;
|
||||
private String bypassPermission;
|
||||
private String activated;
|
||||
private String deactivated;
|
||||
private String notification;
|
||||
private String consoleLog;
|
||||
public static final class Webhook {
|
||||
private String url;
|
||||
private String username;
|
||||
private String avatarUrl;
|
||||
private String content;
|
||||
|
||||
private final Footer footer = new Footer();
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Footer {
|
||||
private String text;
|
||||
private String iconUrl;
|
||||
}
|
||||
|
||||
private final Embed embed = new Embed();
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static final class Embed {
|
||||
private String title;
|
||||
private String titleUrl;
|
||||
@Setter
|
||||
private String description;
|
||||
private int r, g, b;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private @Nullable DiscordWebhook discordWebhook;
|
||||
|
||||
public void load() {
|
||||
if (generalConfig == null) {
|
||||
generalConfig = new SimpleYamlConfig(pluginFolder, "config");
|
||||
@@ -219,23 +287,32 @@ public final class SonarConfiguration {
|
||||
|
||||
// General options
|
||||
generalConfig.getYaml().setComment("language",
|
||||
"Name of the language file Sonar should use for messages"
|
||||
);
|
||||
"Name of the language file Sonar should use for messages");
|
||||
String language = generalConfig.getString("language", "en");
|
||||
|
||||
generalConfig.getYaml().setComment("max-online-per-ip",
|
||||
"Maximum number of players online with the same IP address"
|
||||
);
|
||||
"Maximum number of players online with the same IP address");
|
||||
maxOnlinePerIp = clamp(generalConfig.getInt("max-online-per-ip", 3), 1, Byte.MAX_VALUE);
|
||||
|
||||
generalConfig.getYaml().setComment("min-players-for-attack",
|
||||
"Minimum number of new players in order for an attack to be detected"
|
||||
);
|
||||
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 5), 2, 1024);
|
||||
"Minimum number of new players in order for an attack to be detected");
|
||||
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 8), 2, 1024);
|
||||
|
||||
generalConfig.getYaml().setComment("min-attack-duration",
|
||||
"Amount of time (in milliseconds) that has to pass in order for an attack to be over");
|
||||
minAttackDuration = clamp(generalConfig.getInt("min-attack-duration", 30000), 1000, 900000);
|
||||
|
||||
generalConfig.getYaml().setComment("min-attack-threshold",
|
||||
"Number of times an incident has to be reported in order to be acknowledged as an attack"
|
||||
+ LINE_SEPARATOR + "This number acts as a buffer to filter out false attack notifications");
|
||||
minAttackThreshold = clamp(generalConfig.getInt("min-attack-threshold", 2), 0, 20);
|
||||
|
||||
generalConfig.getYaml().setComment("attack-cooldown-delay",
|
||||
"Amount of time (in milliseconds) that has to pass in order for a new attack to be detected");
|
||||
attackCooldownDelay = clamp(generalConfig.getInt("attack-cooldown-delay", 3000), 100, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("log-player-addresses",
|
||||
"Should Sonar log players' IP addresses in console?"
|
||||
);
|
||||
"Should Sonar log players' IP addresses in console?");
|
||||
logPlayerAddresses = generalConfig.getBoolean("log-player-addresses", true);
|
||||
|
||||
// Message settings
|
||||
@@ -264,10 +341,9 @@ public final class SonarConfiguration {
|
||||
// Database
|
||||
generalConfig.getYaml().setComment("database.type",
|
||||
"Type of database Sonar uses to store verified players"
|
||||
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL, MARIADB (experimental)"
|
||||
);
|
||||
final String newDatabaseType = generalConfig.getString("database.type", Database.Type.NONE.name());
|
||||
database.type = Database.Type.valueOf(newDatabaseType.toUpperCase());
|
||||
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL");
|
||||
database.type = Database.Type.valueOf(
|
||||
generalConfig.getString("database.type", Database.Type.NONE.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("database",
|
||||
"You can connect Sonar to a database to keep verified players even after restarting your server"
|
||||
@@ -295,27 +371,6 @@ public final class SonarConfiguration {
|
||||
"Password for authenticating with the SQL database");
|
||||
database.password = generalConfig.getString("database.password", "");
|
||||
|
||||
// Lockdown
|
||||
generalConfig.getYaml().setComment("lockdown",
|
||||
"You can lock your server down using '/sonar lockdown' if, for example,"
|
||||
+ LINE_SEPARATOR + "bots are bypassing the verification or any other reason");
|
||||
generalConfig.getYaml().setComment("lockdown.enabled",
|
||||
"Should Sonar prevent all players from joining the server?");
|
||||
lockdown.enabled = generalConfig.getBoolean("lockdown.enabled", false);
|
||||
|
||||
generalConfig.getYaml().setComment("lockdown.log-attempts",
|
||||
"Should Sonar log new login attempts during lockdown?");
|
||||
lockdown.logAttempts = generalConfig.getBoolean("lockdown.log-attempts", true);
|
||||
|
||||
generalConfig.getYaml().setComment("lockdown.notify-admins",
|
||||
"Should Sonar notify admins when they join the server during lockdown?");
|
||||
lockdown.notifyAdmins = generalConfig.getBoolean("lockdown.notify-admins", true);
|
||||
|
||||
generalConfig.getYaml().setComment("lockdown.bypass-permission",
|
||||
"Which permission does a player need in order to bypass the lockdown mode?"
|
||||
+ LINE_SEPARATOR + "Players with this permission will also receive admin notifications when joining");
|
||||
lockdown.bypassPermission = generalConfig.getString("lockdown.bypass-permission", "sonar.lockdown.bypass");
|
||||
|
||||
// Queue
|
||||
generalConfig.getYaml().setComment("queue",
|
||||
"Every new login request will be queued to avoid spam join attacks"
|
||||
@@ -328,37 +383,106 @@ public final class SonarConfiguration {
|
||||
generalConfig.getYaml().setComment("verification",
|
||||
"Every new player that joins for the first time will be sent to"
|
||||
+ LINE_SEPARATOR + "a lightweight limbo server where advanced bot checks are performed");
|
||||
generalConfig.getYaml().setComment("verification.enabled",
|
||||
"Should Sonar verify new players? (Recommended)");
|
||||
verification.enabled = generalConfig.getBoolean("verification.enabled", true);
|
||||
generalConfig.getYaml().setComment("verification.timing",
|
||||
"When should Sonar verify new players?"
|
||||
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
|
||||
+ LINE_SEPARATOR + "- ALWAYS: New players will always be checked (Recommended)"
|
||||
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only be checked during an attack"
|
||||
+ LINE_SEPARATOR + "- NEVER: New players will never be checked");
|
||||
verification.timing = Verification.Timing.valueOf(
|
||||
generalConfig.getString("verification.timing", Verification.Timing.ALWAYS.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity",
|
||||
"Checks if the players' falling motion is following Minecraft's gravity formula"
|
||||
+ LINE_SEPARATOR + "All predicted motions are precalculated in order to save performance");
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.enabled",
|
||||
"Should Sonar check for valid client gravity? (Recommended)");
|
||||
verification.checkGravity = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.max-movement-ticks",
|
||||
"Maximum number of ticks the player has to fall in order to be allowed to hit the platform");
|
||||
verification.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8),
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity" +
|
||||
".max-movement-ticks", 8),
|
||||
2, 100);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.max-ignored-ticks",
|
||||
"Maximum number of ignored Y movement changes before a player fails verification");
|
||||
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1,
|
||||
128);
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"
|
||||
, 5),
|
||||
1, 128);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.collisions",
|
||||
"Checks if the players collides with barrier blocks spawned below the player"
|
||||
+ LINE_SEPARATOR + "Note: The collision check will be skipped if the gravity check is disabled");
|
||||
generalConfig.getYaml().setComment("verification.checks.collisions.enabled",
|
||||
"Should Sonar check for valid client collisions? (Recommended)");
|
||||
verification.checkCollisions = generalConfig.getBoolean("verification.checks.collisions.enabled", true);
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.gamemode",
|
||||
"The gamemode of the player during verification"
|
||||
+ LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"
|
||||
+ LINE_SEPARATOR + "- SURVIVAL: all UI components are visible"
|
||||
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
|
||||
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible");
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(
|
||||
generalConfig.getString("verification.checks.gravity.gamemode", Verification.Gravity.Gamemode.ADVENTURE.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.gamemode",
|
||||
"The gamemode of the player during verification (0, 1, 2, or 3)");
|
||||
verification.gamemodeId = (short) clamp(generalConfig.getInt("verification.gamemode", 3), 0, 3);
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha",
|
||||
"Make the player type a code from a virtual map in chat after the gravity check");
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.enabled",
|
||||
"Should Sonar make the player pass a captcha?");
|
||||
verification.map.enabled = generalConfig.getBoolean("verification.checks.map-captcha.enabled", false);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.background-noise",
|
||||
"Should Sonar draw a white/light gray rectangle behind the captcha?");
|
||||
verification.map.drawBackgroundNoise = generalConfig.getBoolean("verification.checks.map-captcha" +
|
||||
".background-noise", true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-position",
|
||||
"Should Sonar randomize the X and Y position of the captcha?");
|
||||
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position",
|
||||
true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-font-size",
|
||||
"Should Sonar randomize the size of the font used for rendering the captcha?");
|
||||
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size",
|
||||
true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.precompute",
|
||||
"How many answers should Sonar precompute (prepare)?");
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 1000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration",
|
||||
"How long should Sonar wait until the player fails the captcha?");
|
||||
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 45000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-tries",
|
||||
"How many times must a player fail the captcha before failing the verification?");
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries", 3);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary",
|
||||
"Characters (letters and numbers) that are allowed to appear in the answer to the captcha");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "123456789");
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
|
||||
"Regex for validating usernames during verification");
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.checks.valid-name-regex", "^[a-zA-Z0-9_]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-brand-regex",
|
||||
"Regex for validating client brands during verification");
|
||||
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.checks.valid-brand-regex", "^[!-~ ]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-locale-regex",
|
||||
"Regex for validating client locale during verification");
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.checks.valid-locale-regex", "^[a-zA-Z_]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.max-brand-length",
|
||||
"Maximum client brand length during verification");
|
||||
verification.maxBrandLength = generalConfig.getInt("verification.checks.max-brand-length", 64);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.max-ping",
|
||||
"Ping (in milliseconds) a player has to have in order to timeout");
|
||||
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping", 10000), 500, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.max-login-packets",
|
||||
"Maximum number of login packets the player has to send in order to be kicked");
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets", 256), 128, 8192);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.log-connections",
|
||||
"Should Sonar log new verification attempts?");
|
||||
@@ -373,46 +497,108 @@ public final class SonarConfiguration {
|
||||
+ LINE_SEPARATOR + "This is not recommended for production servers but can be helpful for spotting errors.");
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions", false);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.valid-name-regex",
|
||||
"Regex for validating usernames during verification");
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.valid-name-regex", "^[a-zA-Z0-9_.*!]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.valid-brand-regex",
|
||||
"Regex for validating client brands during verification");
|
||||
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.valid-brand-regex", "^[!-~ ]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.valid-locale-regex",
|
||||
"Regex for validating client locale during verification");
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.valid-locale-regex", "^[a-zA-Z_]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-brand-length",
|
||||
"Maximum client brand length during verification");
|
||||
verification.maxBrandLength = generalConfig.getInt("verification.max-brand-length", 64);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-ping",
|
||||
"Ping (in milliseconds) a player has to have in order to timeout");
|
||||
verification.maxPing = clamp(generalConfig.getInt("verification.max-ping", 10000), 500, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.read-timeout",
|
||||
"Amount of time that has to pass before a player times out");
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout", 3500), 500, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-login-packets",
|
||||
"Maximum number of login packets the player has to send in order to be kicked");
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.max-login-packets", 256), 128, 8192);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-players",
|
||||
"Maximum number of players verifying at the same time");
|
||||
verification.maxVerifyingPlayers = clamp(generalConfig.getInt("verification.max-players", 1024), 1,
|
||||
Short.MAX_VALUE);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.rejoin-delay",
|
||||
"Minimum number of rejoin delay during verification");
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay", 8000), 0, 100000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.whitelisted-protocols",
|
||||
"List of protocol IDs which are not checked by Sonar (verification bypass)"
|
||||
+ LINE_SEPARATOR + "You can find the full list of all protocol IDs here:"
|
||||
+ LINE_SEPARATOR + "https://wiki.vg/Protocol_version_numbers"
|
||||
+ LINE_SEPARATOR + "For example, Minecraft 1.20 has the ID 763.");
|
||||
verification.whitelistedProtocols.clear();
|
||||
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols",
|
||||
new ArrayList<>(0)));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.blacklisted-protocols",
|
||||
"List of protocol IDs which are unable to join the server at all");
|
||||
verification.blacklistedProtocols.clear();
|
||||
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols",
|
||||
new ArrayList<>(0)));
|
||||
|
||||
generalConfig.getYaml().setComment("webhook",
|
||||
"Bot attack notifications can also be sent to your Discord server using webhooks");
|
||||
generalConfig.getYaml().setComment("webhook.url",
|
||||
"URL of the Discord webhook (Set this to '' to disable webhooks)");
|
||||
webhook.url = generalConfig.getString("webhook.url", "");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.username",
|
||||
"Username of the Discord webhook sender");
|
||||
webhook.username = generalConfig.getString("webhook.username", "Sonar");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.avatar-url",
|
||||
"URL to the avatar of the Discord webhook sender (Set this to '' to disable)");
|
||||
webhook.avatarUrl = generalConfig.getString("webhook.avatar-url", "");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.content",
|
||||
"Content of the Discord webhook message (Set this to '' to disable)"
|
||||
+ LINE_SEPARATOR + "You can use this to e.g. ping staff members using <@userId>"
|
||||
+ LINE_SEPARATOR + "If you want to ping roles, you will need to use <@&roleId>");
|
||||
webhook.content = generalConfig.getString("webhook.content", "");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.embed.footer",
|
||||
"Small footer message of the Discord webhook embed");
|
||||
generalConfig.getYaml().setComment("webhook.embed.footer.text",
|
||||
"Content of the footer message of the Discord webhook embed");
|
||||
webhook.footer.text = generalConfig.getString("webhook.embed.footer.text",
|
||||
"© Jones Development and Sonar Contributors");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.embed.footer.icon-url",
|
||||
"URL of the footer message icon of the Discord webhook embed");
|
||||
webhook.footer.iconUrl = generalConfig.getString("webhook.embed.footer.icon-url", "");
|
||||
|
||||
final String realEmbedPath = "webhook.embed";
|
||||
final String embedPath = realEmbedPath + ".";
|
||||
|
||||
generalConfig.getYaml().setComment(realEmbedPath,
|
||||
"Embed Discord webhook message that is sent when an attack has stopped");
|
||||
generalConfig.getYaml().setComment(embedPath + "title",
|
||||
"Title of the Discord webhook embed");
|
||||
webhook.embed.title = generalConfig.getString(embedPath + "title", ":white_check_mark: Attack mitigated");
|
||||
|
||||
generalConfig.getYaml().setComment(embedPath + "title-url",
|
||||
"Clickable URL of the title of the Discord webhook embed");
|
||||
webhook.embed.titleUrl = generalConfig.getString(embedPath + "title-url", "");
|
||||
|
||||
generalConfig.getYaml().setComment(embedPath + "description",
|
||||
"Description (content) of the Discord webhook embed");
|
||||
webhook.embed.description = fromList(generalConfig.getStringList(embedPath + "description", Arrays.asList(
|
||||
"The attack on your server has been mitigated.",
|
||||
"",
|
||||
"Attack start: <t:%start-timestamp%:T>",
|
||||
"Attack end: <t:%end-timestamp%:T>",
|
||||
"Attack duration: %duration%",
|
||||
"",
|
||||
"Peak process CPU usage during the attack: %peak-cpu%%",
|
||||
"Peak process memory usage during the attack: %peak-memory%",
|
||||
"Peak bots per second during the attack: %peak-bps%",
|
||||
"",
|
||||
"Blacklisted IP addresses during the attack: %total-blacklisted%",
|
||||
"Failed verifications during the attack: %total-failed%",
|
||||
"Successful verifications during the attack: %total-success%"
|
||||
)), LINE_SEPARATOR);
|
||||
|
||||
generalConfig.getYaml().setComment(embedPath + "color",
|
||||
"RGB colors of the Discord webhook embed"
|
||||
+ LINE_SEPARATOR + "Color picker: https://www.rapidtables.com/web/color/RGB_Color.html");
|
||||
webhook.embed.r = generalConfig.getInt(embedPath + "color.red", 0);
|
||||
webhook.embed.g = generalConfig.getInt(embedPath + "color.green", 255);
|
||||
webhook.embed.b = generalConfig.getInt(embedPath + "color.blue", 0);
|
||||
|
||||
if (!webhook.url.isEmpty()) {
|
||||
if (webhook.username.isEmpty()) {
|
||||
throw new IllegalStateException("Webhook username cannot be empty");
|
||||
}
|
||||
discordWebhook = new DiscordWebhook(webhook.url);
|
||||
} else if (discordWebhook != null) {
|
||||
// Reset if Discord webhooks were disabled
|
||||
discordWebhook = null;
|
||||
}
|
||||
|
||||
// load this here otherwise it could cause issues
|
||||
messagesConfig.getYaml().setComment("header",
|
||||
"Placeholder for every '%header%' in this configuration file");
|
||||
@@ -448,7 +634,7 @@ public final class SonarConfiguration {
|
||||
"%prefix%<red>Usage: /sonar %usage%"));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.invalid-ip-address",
|
||||
"Message that is shown when someone provides an invalid IP address (Invalid characters)");
|
||||
"Message that is shown when someone provides an invalid IP address (Invalid format)");
|
||||
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address",
|
||||
"%prefix%The IP address you provided seems to be invalid."));
|
||||
|
||||
@@ -457,11 +643,6 @@ public final class SonarConfiguration {
|
||||
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address",
|
||||
"%prefix%The IP address you provided seems to be either a local or loopback IP."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.unknown-ip-address",
|
||||
"Message that is shown when someone provides an unknown IP address (Unknown Host)");
|
||||
commands.unknownIpAddress = formatString(messagesConfig.getString("commands.unknown-ip-address",
|
||||
"%prefix%The IP address you provided seems to be unknown."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.player-only",
|
||||
"Message that is shown when the console runs a command that is player-only");
|
||||
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only",
|
||||
@@ -484,28 +665,32 @@ public final class SonarConfiguration {
|
||||
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission",
|
||||
"%prefix%<red>You do not have permission to execute this subcommand. <gray>(%permission%)"));
|
||||
|
||||
messagesConfig.getYaml().setComment("lockdown",
|
||||
"Translations for '/sonar lockdown'");
|
||||
messagesConfig.getYaml().setComment("lockdown.notification",
|
||||
"Message that is shown when an admin joins the server during lockdown");
|
||||
lockdown.notification = formatString(messagesConfig.getString("lockdown.notification",
|
||||
"%prefix%<green>Hey, the server is currently in lockdown mode. If you want to disable the lockdown mode,"
|
||||
+ " type <white>/sonar lockdown<green>."
|
||||
));
|
||||
|
||||
messagesConfig.getYaml().setComment("lockdown.console-log",
|
||||
"Message that is shown to console when a normal player tries joining the server during lockdown");
|
||||
lockdown.consoleLog = messagesConfig.getString("lockdown.console-log",
|
||||
"%player% (%ip%, %protocol%) tried to join during lockdown mode.");
|
||||
|
||||
messagesConfig.getYaml().setComment("lockdown.disconnect-message",
|
||||
"Message that is shown to a normal player when they try joining the server during lockdown");
|
||||
lockdown.disconnect = deserialize(fromList(messagesConfig.getStringList("lockdown.disconnect-message",
|
||||
messagesConfig.getYaml().setComment("commands.main",
|
||||
"Translations for '/sonar'");
|
||||
messagesConfig.getYaml().setComment("commands.main.header",
|
||||
"Informational message that is shown above everything when running the main command");
|
||||
commands.helpHeader = messagesConfig.getStringList("commands.main.header",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<red>The server is currently locked down, please try again later.",
|
||||
"%footer%"
|
||||
))));
|
||||
"<yellow>Running Sonar %version% on %platform%.",
|
||||
"<yellow>(C) %copyright_year% Jones Development and Sonar Contributors",
|
||||
"<green><click:open_url:'https://github.com/jonesdevelopment/sonar'>https://github.com/jonesdevelopment/sonar",
|
||||
"",
|
||||
"<yellow>Need help or have any questions?",
|
||||
"<yellow><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click to open Discord)'>Open a " +
|
||||
"ticket on the Discord </hover></click></yellow><yellow><click:open_url:'https://github" +
|
||||
".com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click to open GitHub)'>or open a new issue on " +
|
||||
"GitHub.",
|
||||
""
|
||||
));
|
||||
messagesConfig.getYaml().setComment("commands.main.subcommands",
|
||||
"Formatting of the list of subcommands shown when running the main command");
|
||||
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands",
|
||||
"<click:suggest_command:'/sonar %subcommand% '><hover:show_text:'<gray>Only players: " +
|
||||
"</gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission: " +
|
||||
"</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%'><gray> ▪ </gray><green>/sonar " +
|
||||
"%subcommand%</green><gray> - </gray><white>%description%"));
|
||||
|
||||
SonarCommand.prepareCachedMessages();
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.reload",
|
||||
"Translations for '/sonar reload'");
|
||||
@@ -519,18 +704,6 @@ public final class SonarConfiguration {
|
||||
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish",
|
||||
"%prefix%<green>Successfully reloaded <gray>(%taken%ms)"));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.lockdown",
|
||||
"Translations for '/sonar lockdown'");
|
||||
messagesConfig.getYaml().setComment("commands.lockdown.enabled",
|
||||
"Message that is shown when a player enables server lockdown");
|
||||
lockdown.activated = formatString(messagesConfig.getString("commands.lockdown.enabled",
|
||||
"%prefix%The server is now in lockdown mode."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.lockdown.disabled",
|
||||
"Message that is shown when a player disables server lockdown");
|
||||
lockdown.deactivated = formatString(messagesConfig.getString("commands.lockdown.disabled",
|
||||
"%prefix%The server is no longer in lockdown mode."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.verbose",
|
||||
"Translations for '/sonar verbose'");
|
||||
messagesConfig.getYaml().setComment("commands.verbose.subscribed",
|
||||
@@ -543,16 +716,6 @@ public final class SonarConfiguration {
|
||||
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed",
|
||||
"%prefix%You are no longer viewing Sonar verbose."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.verbose.subscribed-other",
|
||||
"Message that is shown when a player makes another player subscribe to Sonar verbose");
|
||||
commands.verboseSubscribedOther = formatString(messagesConfig.getString("commands.verbose.subscribed-other",
|
||||
"%prefix%%player% is now viewing Sonar verbose."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.verbose.unsubscribed-other",
|
||||
"Message that is shown when a player makes another player unsubscribe from Sonar verbose");
|
||||
commands.verboseUnsubscribedOther = formatString(messagesConfig.getString("commands.verbose.unsubscribed-other",
|
||||
"%prefix%%player% is no longer viewing Sonar verbose."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.blacklist",
|
||||
"Translations for '/sonar blacklist'");
|
||||
messagesConfig.getYaml().setComment("commands.blacklist.empty",
|
||||
@@ -709,6 +872,25 @@ public final class SonarConfiguration {
|
||||
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful",
|
||||
"%name% has been verified successfully (%time%s!)."));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.welcome",
|
||||
"Message that is shown to the player when they are being checked for valid gravity");
|
||||
verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome",
|
||||
"%prefix%<gray>Please wait a moment for the verification to finish...")));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.captcha.enter-code",
|
||||
"Message that is shown to the player when they have to enter the answer to the captcha");
|
||||
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code",
|
||||
"%prefix%<green>Please enter the code in chat that is displayed on the map.")));
|
||||
messagesConfig.getYaml().setComment("verification.captcha.action-bar",
|
||||
"Timer that is shown to the player when they have to enter the answer to the captcha"
|
||||
+ LINE_SEPARATOR + "(Set this to '' to disable the action bar message)");
|
||||
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar",
|
||||
"%prefix%<green>You have %time-left% seconds left to enter the code in chat"));
|
||||
messagesConfig.getYaml().setComment("verification.captcha.incorrect",
|
||||
"Message that is shown to the player when they enter the wrong answer in chat");
|
||||
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect",
|
||||
"%prefix%<red>You have entered the wrong code. Please try again.")));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.too-many-players",
|
||||
"Disconnect message that is shown when too many players are verifying at the same time");
|
||||
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players",
|
||||
@@ -778,6 +960,17 @@ public final class SonarConfiguration {
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.blacklisted-protocol",
|
||||
"Disconnect message that is shown when someone joins with a blacklisted version");
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification" +
|
||||
".blacklisted-protocol",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<red>You are using a version that is not allowed on our server.",
|
||||
"<gold>Need help logging in? <gray>%support-url%",
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.already-online",
|
||||
"Disconnect message that is shown when someone tries verifying with an account that is online");
|
||||
verification.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online",
|
||||
@@ -811,28 +1004,29 @@ public final class SonarConfiguration {
|
||||
|
||||
messagesConfig.getYaml().setComment("verbose",
|
||||
"Translations for all messages regarding Sonar's verbose output");
|
||||
messagesConfig.getYaml().setComment("verbose.layout",
|
||||
"General layout for the verbose action-bar" +
|
||||
LINE_SEPARATOR + "Placeholders and their descriptions:" +
|
||||
LINE_SEPARATOR + "- %queued% Number of queued connections" +
|
||||
LINE_SEPARATOR + "- %verifying% Number of verifying connections" +
|
||||
LINE_SEPARATOR + "- %blacklisted% Number of blacklisted IP addresses" +
|
||||
LINE_SEPARATOR + "- %total-joins% Number of total joins (not unique!)" +
|
||||
LINE_SEPARATOR + "- %per-second-joins% Number of joins per second" +
|
||||
LINE_SEPARATOR + "- %verify-total% Number of total verification attempts" +
|
||||
LINE_SEPARATOR + "- %verify-success% Number of verified IP addresses" +
|
||||
LINE_SEPARATOR + "- %verify-failed% Number of failed verifications" +
|
||||
LINE_SEPARATOR + "- %incoming-traffic% Incoming bandwidth usage per second" +
|
||||
LINE_SEPARATOR + "- %outgoing-traffic% Outgoing bandwidth usage per second" +
|
||||
LINE_SEPARATOR + "- %incoming-traffic-ttl% Total incoming bandwidth usage" +
|
||||
LINE_SEPARATOR + "- %outgoing-traffic-ttl% Total outgoing bandwidth usage" +
|
||||
LINE_SEPARATOR + "- %used-memory% Amount of used memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %total-memory% Amount of total memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %max-memory% Amount of max memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %free-memory% Amount of free memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %animation% Animated spinning circle (by default)"
|
||||
);
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout",
|
||||
messagesConfig.getYaml().setComment("verbose.layout.normal",
|
||||
"General layout for the verbose action-bar"
|
||||
+ LINE_SEPARATOR + "Placeholders:"
|
||||
+ LINE_SEPARATOR + "- '%queued%' Number of queued connections"
|
||||
+ LINE_SEPARATOR + "- '%verifying%' Number of verifying connections"
|
||||
+ LINE_SEPARATOR + "- '%blacklisted%' Number of blacklisted IP addresses"
|
||||
+ LINE_SEPARATOR + "- '%total-joins%' Number of total attempted joins"
|
||||
+ LINE_SEPARATOR + "- '%verify-total%' Number of total verification attempts"
|
||||
+ LINE_SEPARATOR + "- '%verify-success%' Number of verified IP addresses"
|
||||
+ LINE_SEPARATOR + "- '%verify-failed%' Number of failed verifications"
|
||||
+ LINE_SEPARATOR + "- '%logins-per-second%' Number of logins per second"
|
||||
+ LINE_SEPARATOR + "- '%attack-duration%' Duration of the current attack"
|
||||
+ LINE_SEPARATOR + "- '%incoming-traffic%' Incoming bandwidth usage per second"
|
||||
+ LINE_SEPARATOR + "- '%outgoing-traffic%' Outgoing bandwidth usage per second"
|
||||
+ LINE_SEPARATOR + "- '%incoming-traffic-ttl%' Total incoming bandwidth usage"
|
||||
+ LINE_SEPARATOR + "- '%outgoing-traffic-ttl%' Total outgoing bandwidth usage"
|
||||
+ LINE_SEPARATOR + "- '%used-memory%' Amount of used memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%total-memory%' Amount of total memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%max-memory%' Amount of max memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%free-memory%' Amount of free memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%animation%' Customizable animated symbol"
|
||||
+ LINE_SEPARATOR + "Translations for Sonar's normal verbose output");
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal",
|
||||
String.join(" <dark_aqua>╺ ", Arrays.asList(
|
||||
"%prefix%<gray>Queued <white>%queued%",
|
||||
"<gray>Verifying <white>%verifying%",
|
||||
@@ -840,12 +1034,22 @@ public final class SonarConfiguration {
|
||||
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
|
||||
" <green><bold>%animation%<reset>"
|
||||
))));
|
||||
messagesConfig.getYaml().setComment("verbose.animation",
|
||||
"Alternative symbols:"
|
||||
+ LINE_SEPARATOR + "- ▙"
|
||||
+ LINE_SEPARATOR + "- ▛"
|
||||
+ LINE_SEPARATOR + "- ▜"
|
||||
+ LINE_SEPARATOR + "- ▟");
|
||||
messagesConfig.getYaml().setComment("verbose.layout.attack",
|
||||
"Translations for Sonar's verbose output during an active attack");
|
||||
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack",
|
||||
String.join(" <dark_aqua>╺ ", Arrays.asList(
|
||||
"%prefix%<gray>BPS <white>%logins-per-second%",
|
||||
"<gray>Queued <white>%queued%",
|
||||
"<gray>Verifying <white>%verifying%",
|
||||
"<gray>Blacklisted <white>%blacklisted%",
|
||||
"<gray>Duration <white>%attack-duration%" +
|
||||
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
|
||||
" <green><bold>%animation%<reset>"
|
||||
))));
|
||||
messagesConfig.getYaml().setComment("verbose.animation", "Animation for the action bar"
|
||||
+ LINE_SEPARATOR + "Alternatives:"
|
||||
+ LINE_SEPARATOR + "- ▙, ▛, ▜, ▟"
|
||||
+ LINE_SEPARATOR + "- ⬈, ⬊, ⬋, ⬉");
|
||||
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation",
|
||||
Arrays.asList("◜", "◝", "◞", "◟")
|
||||
));
|
||||
@@ -866,7 +1070,11 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list) {
|
||||
return formatString(String.join("<newline>", list));
|
||||
return fromList(list, "<newline>");
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list, final String newline) {
|
||||
return formatString(String.join(newline, list));
|
||||
}
|
||||
|
||||
private static @NotNull Component deserialize(final String legacy) {
|
||||
|
||||
+12
-2
@@ -65,6 +65,8 @@ public final class VerifiedPlayerController {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException exception) {
|
||||
// Duplicate index
|
||||
// I know this isn't the best method of handling it,
|
||||
// but I don't know how else I could address this issue.
|
||||
}
|
||||
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
@@ -92,7 +94,11 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
Objects.requireNonNull(connectionSource);
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
|
||||
@@ -134,7 +140,11 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
Objects.requireNonNull(connectionSource);
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dao.create(player);
|
||||
|
||||
@@ -38,8 +38,9 @@ public enum Dependency {
|
||||
MYSQL(
|
||||
"com/mysql",
|
||||
"mysql-connector-j",
|
||||
"8.1.0"
|
||||
"8.2.0"
|
||||
),
|
||||
// TODO: MariaDB still seems to have some issues; investigate!
|
||||
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
|
||||
MARIADB(
|
||||
"org/mariadb/jdbc",
|
||||
@@ -47,10 +48,8 @@ public enum Dependency {
|
||||
"3.1.4"
|
||||
);
|
||||
|
||||
private @NotNull
|
||||
final Path tempFilePath;
|
||||
private @NotNull
|
||||
final URL mvnRepoURL;
|
||||
private final Path tempFilePath;
|
||||
private final URL mvnRepoURL;
|
||||
|
||||
Dependency(final @NotNull String groupId,
|
||||
final @NotNull String artifactId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.j256.ormlite.support.ConnectionSource;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
@@ -36,44 +37,42 @@ import java.util.Properties;
|
||||
@UtilityClass
|
||||
public class DependencyLoader {
|
||||
public ConnectionSource setUpDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration config = Sonar.get().getConfig();
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
final URL[] urls = new URL[config.getDatabase().getType().getDependencies().length];
|
||||
for (final Dependency dependency : config.getDatabase().getType().getDependencies()) {
|
||||
final URL url = dependency.getClassLoaderURL();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
ClassLoader classLoader = currentClassLoader;
|
||||
|
||||
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
|
||||
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
|
||||
final URL url = database.getType().getDependency().getClassLoaderURL();
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
urls[dependency.ordinal()] = url;
|
||||
classLoader = new ExternalClassLoader(new URL[]{url});
|
||||
}
|
||||
|
||||
final String type = config.getDatabase().getType().name().toLowerCase();
|
||||
final String databaseURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
type, config.getDatabase().getUrl(), config.getDatabase().getPort(), config.getDatabase().getName());
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
|
||||
|
||||
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
|
||||
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
|
||||
return new JdbcSingleConnectionSource(databaseURL, connection);
|
||||
final Connection connection = connect(classLoader, jdbcURL, database);
|
||||
return new JdbcSingleConnectionSource(jdbcURL, connection);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
|
||||
private Connection connect(final @NotNull ClassLoader classLoader,
|
||||
final @NotNull String databaseURL,
|
||||
final @NotNull SonarConfiguration.Database config) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
|
||||
final @NotNull SonarConfiguration.Database database) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.put("user", config.getUsername());
|
||||
if (!config.getPassword().isEmpty()) {
|
||||
properties.put("password", config.getPassword());
|
||||
}
|
||||
properties.put("user", database.getUsername());
|
||||
properties.put("password", database.getPassword());
|
||||
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
|
||||
@@ -21,24 +21,40 @@ import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@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);
|
||||
private static final ExecutorService EVENT_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
// Don't post a task if there are no listeners
|
||||
if (EVENT_LISTENERS.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EVENT_SERVICE.execute(() -> {
|
||||
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
|
||||
try {
|
||||
eventListener.handle(event);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not pass {} to listener: {}",
|
||||
event.getClass().getSimpleName(), throwable);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +63,7 @@ public final class SonarEventManager {
|
||||
* @param listeners One (or more) listeners to register
|
||||
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
|
||||
*/
|
||||
@SuppressWarnings("unused") // External API usage
|
||||
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
|
||||
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
|
||||
}
|
||||
@@ -58,6 +75,7 @@ public final class SonarEventManager {
|
||||
* @apiNote This does not have any effect if the given listeners are not registered
|
||||
* @see #registerListener(SonarEventListener...) Register an event listener
|
||||
*/
|
||||
@SuppressWarnings("unused") // External API usage
|
||||
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
|
||||
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
|
||||
}
|
||||
|
||||
+3
-21
@@ -15,27 +15,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.verbose;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class ActionBarVerbose extends Verbose {
|
||||
private final Server server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final Component component) {
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
final Player player = server.getPlayer(subscriber);
|
||||
if (player != null) {
|
||||
// TODO: use an adventure audience
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public final class AttackDetectedEvent implements SonarEvent {
|
||||
}
|
||||
+9
-18
@@ -15,28 +15,19 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.server;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ServerWrapper {
|
||||
|
||||
/**
|
||||
* Platform of the server (Velocity, BungeeCord, Spigot)
|
||||
*/
|
||||
private final SonarPlatform platform;
|
||||
|
||||
/**
|
||||
* @param username Username of the player
|
||||
* @return Optional player wrapped as InvocationSender
|
||||
* @see InvocationSource
|
||||
*/
|
||||
public abstract Optional<InvocationSource> getOnlinePlayer(final String username);
|
||||
public final class AttackMitigatedEvent implements SonarEvent {
|
||||
private final AttackTracker.AttackStatistics attackStatistics;
|
||||
}
|
||||
+10
-3
@@ -19,16 +19,23 @@ package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final String reason;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser<?, ?> user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,12 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
@@ -38,11 +40,10 @@ public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
|
||||
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 2500L
|
||||
);
|
||||
private final ExpiringCache<InetAddress> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 5000L);
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
|
||||
@@ -64,8 +65,11 @@ public final class Fallback {
|
||||
}
|
||||
};
|
||||
|
||||
public boolean isPotentiallyUnderAttack() {
|
||||
final int min = SONAR.getConfig().getMinPlayersForAttack();
|
||||
return connected.size() > min || queue.getQueuedPlayers().size() > min;
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
val timing = Sonar.get().getConfig().getVerification().getTiming();
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
}
|
||||
}
|
||||
@@ -50,14 +50,14 @@ public final class FallbackQueue {
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
final int max = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
// We need to be very careful here since we don't want any concurrency issues.
|
||||
// We need to be cautious here since we don't want any concurrency issues.
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// Break if we reached our maximum entries
|
||||
if (++index > max) {
|
||||
if (++index > maxQueuePolls) {
|
||||
break;
|
||||
}
|
||||
// Run the cached runnable
|
||||
@@ -66,5 +66,10 @@ public final class FallbackQueue {
|
||||
// Remove runnable from iterator
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
}
|
||||
}
|
||||
@@ -31,18 +31,26 @@ public final class FallbackRatelimiter {
|
||||
@Setter
|
||||
private ExpiringCache<InetAddress> expiringCache;
|
||||
|
||||
/**
|
||||
* We don't want to clean up the cache in the
|
||||
* {@link #attempt(InetAddress)} method because
|
||||
* it would take up too many resources.
|
||||
*/
|
||||
public void cleanUpCache() {
|
||||
expiringCache.cleanUp(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player has tried verifying too fast
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @return Whether the player is allowed to verify
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Clean up the cache
|
||||
expiringCache.cleanUp(false);
|
||||
|
||||
// Cache the IP address if it's not already cached
|
||||
if (!expiringCache.has(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,9 +92,6 @@ public interface FallbackUser<X, Y> {
|
||||
*/
|
||||
default void fail(final @Nullable String reason) {
|
||||
if (getChannel().isActive()) {
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
@@ -107,6 +104,9 @@ public interface FallbackUser<X, Y> {
|
||||
|
||||
Statistics.FAILED_VERIFICATIONS.increment();
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Make sure old entries are removed
|
||||
PREVIOUS_FAILS.cleanUp();
|
||||
|
||||
@@ -115,7 +115,7 @@ public interface FallbackUser<X, Y> {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
getFallback().getBlacklisted().put(getInetAddress().toString());
|
||||
getFallback().getBlacklisted().put(getInetAddress());
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
|
||||
+12
-7
@@ -20,11 +20,9 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ProtocolVersion {
|
||||
UNKNOWN(-1),
|
||||
@@ -67,12 +65,13 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_19_3(761),
|
||||
MINECRAFT_1_19_4(762),
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(764);
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765);
|
||||
|
||||
@Getter
|
||||
private final int protocol;
|
||||
|
||||
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
|
||||
public static final ProtocolVersion LATEST_VERSION;
|
||||
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
|
||||
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
|
||||
|
||||
@@ -96,7 +95,8 @@ public enum ProtocolVersion {
|
||||
}
|
||||
}
|
||||
|
||||
SUPPORTED_VERSIONS = versions;
|
||||
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
|
||||
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +104,15 @@ public enum ProtocolVersion {
|
||||
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
|
||||
}
|
||||
|
||||
public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
|
||||
return compareTo(first) >= 0 && compareTo(last) <= 0;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return this == UNKNOWN;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isLegacy() {
|
||||
return this == LEGACY;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.logger;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
|
||||
/**
|
||||
* The whole purpose of doing this is, so we can have one logger for every module.
|
||||
* We wouldn't need to get the plugin logger every time we try to use it.
|
||||
@@ -28,4 +32,62 @@ public interface LoggerWrapper {
|
||||
void warn(final String message, final Object... args);
|
||||
|
||||
void error(final String message, final Object... args);
|
||||
|
||||
// Taken from
|
||||
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
|
||||
String ARG_STRING = "{}";
|
||||
int ARG_STRING_LENGTH = ARG_STRING.length();
|
||||
Object UNKNOWN_ARG = new Object();
|
||||
|
||||
default String buildFullMessage(final @NotNull String msg, final Object... args) {
|
||||
StringBuilder sb = null;
|
||||
int lastIndex = 0;
|
||||
int argC = 0;
|
||||
while (true) {
|
||||
int argIndex = msg.indexOf(ARG_STRING, lastIndex);
|
||||
// no more {} arguments?
|
||||
if (argIndex == -1) {
|
||||
break;
|
||||
}
|
||||
if (sb == null) {
|
||||
// we build this lazily in case there is no {} in the msg
|
||||
sb = new StringBuilder(128);
|
||||
}
|
||||
// add the string before the arg-string
|
||||
sb.append(msg, lastIndex, argIndex);
|
||||
// shift our last-index past the arg-string
|
||||
lastIndex = argIndex + ARG_STRING_LENGTH;
|
||||
// add the arguments
|
||||
if (argC < args.length) {
|
||||
appendArg(sb, args[argC]);
|
||||
}
|
||||
argC++;
|
||||
}
|
||||
if (sb == null) {
|
||||
return msg;
|
||||
} else {
|
||||
sb.append(msg, lastIndex, msg.length());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
default void appendArg(final StringBuilder stringBuilder, final Object arg) {
|
||||
if (arg == UNKNOWN_ARG) {
|
||||
// ignore it
|
||||
} else if (arg == null) {
|
||||
stringBuilder.append("null");
|
||||
} else if (arg.getClass().isArray()) {
|
||||
stringBuilder.append('[');
|
||||
int length = Array.getLength(arg);
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (i > 0) {
|
||||
stringBuilder.append(", ");
|
||||
}
|
||||
appendArg(stringBuilder, Array.get(arg, i));
|
||||
}
|
||||
stringBuilder.append(']');
|
||||
} else {
|
||||
stringBuilder.append(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,13 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@DatabaseTable(tableName = "verified_players")
|
||||
@DatabaseTable(tableName = "sonar_verified_players")
|
||||
public final class VerifiedPlayer {
|
||||
@SuppressWarnings("unused")
|
||||
@DatabaseField(generatedId = true)
|
||||
@@ -51,12 +52,15 @@ public final class VerifiedPlayer {
|
||||
)
|
||||
private UUID playerUUID;
|
||||
|
||||
@DatabaseField(columnName = "timestamp", canBeNull = false)
|
||||
private long timestamp;
|
||||
@DatabaseField(
|
||||
columnName = "timestamp",
|
||||
canBeNull = false
|
||||
)
|
||||
private Timestamp timestamp;
|
||||
|
||||
public VerifiedPlayer(final String inetAddress, final UUID playerUUID, final long timestamp) {
|
||||
this.inetAddress = inetAddress;
|
||||
this.playerUUID = playerUUID;
|
||||
this.timestamp = timestamp;
|
||||
this.timestamp = new Timestamp(timestamp);
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,16 @@ public interface JVMProfiler {
|
||||
Runtime RUNTIME = Runtime.getRuntime();
|
||||
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
|
||||
char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
|
||||
|
||||
default String formatMemory(final long m) {
|
||||
if (m < 1024L) return m + " B";
|
||||
final int z = (63 - Long.numberOfLeadingZeros(m)) / 10;
|
||||
return String.format("%.1f %sB", (double) m / (1L << (z * 10)), " KMGTPE".charAt(z));
|
||||
default String formatMemory(final long size) {
|
||||
if (size < 1024L) {
|
||||
return size + " B";
|
||||
}
|
||||
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
|
||||
final double formattedSize = (double) size / (1L << (group * 10));
|
||||
final char unit = MEMORY_UNITS[group - 1];
|
||||
return String.format("%.1f %sB", formattedSize, unit);
|
||||
}
|
||||
|
||||
default int getVirtualCores() {
|
||||
|
||||
@@ -18,9 +18,14 @@
|
||||
package xyz.jonesdev.sonar.api.timer;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
@Getter
|
||||
public final class SystemTimer {
|
||||
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
|
||||
private long start = System.currentTimeMillis();
|
||||
|
||||
public void reset() {
|
||||
@@ -35,6 +40,10 @@ public final class SystemTimer {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
public @NotNull String formattedDelay() {
|
||||
return FORMATTER.format(delay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%.3f", delay() / 1000D);
|
||||
|
||||
@@ -19,59 +19,71 @@ package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
|
||||
|
||||
@Getter
|
||||
public abstract class Verbose implements JVMProfiler {
|
||||
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final SystemTimer secondTimer = new SystemTimer();
|
||||
protected int joinsPerSecond, totalJoins;
|
||||
private int lastTotalJoins, animationIndex;
|
||||
public final class Verbose implements JVMProfiler {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
private int animationIndex;
|
||||
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
|
||||
// Run action bar verbose
|
||||
public final void update() {
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
loginsPerSecond.cleanUp(false);
|
||||
|
||||
totalJoins = Statistics.TOTAL_TRAFFIC.get();
|
||||
|
||||
// Statistically determine the joins per second without any caches
|
||||
if (totalJoins > 0 && secondTimer.elapsed(1000L)) {
|
||||
secondTimer.reset();
|
||||
joinsPerSecond = totalJoins - lastTotalJoins;
|
||||
lastTotalJoins = totalJoins;
|
||||
}
|
||||
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
// Prepare the action bar format component
|
||||
final Component component = prepareActionBarFormat();
|
||||
// Send the action bar to all online players
|
||||
broadcast(prepareActionBarFormat());
|
||||
for (final String subscriber : subscribers) {
|
||||
final Audience audience = audiences.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
}
|
||||
}
|
||||
|
||||
protected @NotNull Component prepareActionBarFormat() {
|
||||
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
return MiniMessage.miniMessage().deserialize((attackDuration != null
|
||||
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
|
||||
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
|
||||
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(loginsPerSecond.estimatedSize()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
|
||||
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
|
||||
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
|
||||
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
|
||||
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
|
||||
@@ -83,34 +95,31 @@ public abstract class Verbose implements JVMProfiler {
|
||||
.replace("%animation%", nextAnimation()));
|
||||
}
|
||||
|
||||
protected final String nextAnimation() {
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
// Run action bar verbose
|
||||
protected abstract void broadcast(final Component component);
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player who subscribed
|
||||
* @return Whether the player is subscribed or not
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public final boolean isSubscribed(final @NotNull String subscriber) {
|
||||
return subscribers.contains(subscriber);
|
||||
public boolean isSubscribed(final @NotNull String name) {
|
||||
return subscribers.contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player to subscribe
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
public final void subscribe(final @NotNull String subscriber) {
|
||||
subscribers.add(subscriber);
|
||||
public void subscribe(final String name) {
|
||||
subscribers.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player to unsubscribe
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
public final void unsubscribe(final @NotNull String subscriber) {
|
||||
subscribers.remove(subscriber);
|
||||
public void unsubscribe(final String name) {
|
||||
subscribers.remove(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.webhook;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class DiscordWebhook {
|
||||
private final String url;
|
||||
|
||||
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
|
||||
HTTP_REQUEST_SERVICE.execute(() -> {
|
||||
try {
|
||||
final String serializedContent = prepareSerializedContent(embed.get());
|
||||
final HttpsURLConnection urlConnection = prepareConnection(new URL(url), serializedContent);
|
||||
|
||||
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
|
||||
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
// Check if the connection was a success
|
||||
if (urlConnection.getResponseCode() != 204) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
final String content = Sonar.get().getConfig().getWebhook().getContent();
|
||||
final String username = Sonar.get().getConfig().getWebhook().getUsername();
|
||||
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
|
||||
|
||||
int rgb = embed.getR();
|
||||
rgb = (rgb << 8) + embed.getG();
|
||||
rgb = (rgb << 8) + embed.getB();
|
||||
|
||||
final Webhook.EmbedMessage embedMessage = new Webhook.EmbedMessage(
|
||||
embed.getTitle(), embed.getDescription(), embed.getTitleUrl(), rgb, new Webhook.Field[0],
|
||||
new Webhook.Footer(Sonar.get().getConfig().getWebhook().getFooter().getText(),
|
||||
Sonar.get().getConfig().getWebhook().getFooter().getIconUrl()));
|
||||
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
|
||||
|
||||
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
|
||||
return Sonar.GENERAL_GSON.toJson(webhook);
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
|
||||
final @NotNull String serializedContent) throws Exception {
|
||||
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
// Set connection timeout
|
||||
urlConnection.setConnectTimeout(15000);
|
||||
// Set necessary properties
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setRequestProperty("Content-Length", String.valueOf(serializedContent.length()));
|
||||
urlConnection.setRequestProperty("User-Agent", "Sonar");
|
||||
urlConnection.setDoOutput(true);
|
||||
urlConnection.setRequestMethod("POST");
|
||||
return urlConnection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
static final class Webhook {
|
||||
private final String content, username, avatar_url;
|
||||
private final boolean tts;
|
||||
private final List<EmbedMessage> embeds;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class EmbedMessage {
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final String url;
|
||||
private final int color;
|
||||
private final Field[] fields;
|
||||
private final Footer footer;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class Field {
|
||||
private final String name;
|
||||
private final String value;
|
||||
private final boolean inline;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class Footer {
|
||||
private final String text;
|
||||
private final String icon_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ bukkit {
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
// No permission check here
|
||||
// No permission checks here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,31 +18,31 @@
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
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.BukkitInvocationSource;
|
||||
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), new ActionBarVerbose(plugin.getServer()));
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for Bukkit audiences
|
||||
*/
|
||||
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -53,34 +53,17 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.INFO, message, args);
|
||||
getPlugin().getLogger().info(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.WARNING, message, args);
|
||||
getPlugin().getLogger().warning(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.SEVERE, message, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUKKIT) {
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSource> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getOnlinePlayers().stream()
|
||||
.filter(player -> player.getName().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(BukkitInvocationSource::new);
|
||||
getPlugin().getLogger().severe(buildFullMessage(message, args));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,21 +71,12 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
|
||||
10L, 10L);
|
||||
|
||||
// Register traffic counter reset task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), TrafficCounter::reset,
|
||||
20L, 20L);
|
||||
|
||||
// Register action bar verbose task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
|
||||
2L, 2L);
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -17,16 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
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));
|
||||
super(sender.getName(),
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+9
-129
@@ -17,28 +17,19 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
|
||||
{
|
||||
cacheHelpMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(final CommandSender sender,
|
||||
@@ -47,97 +38,13 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
|
||||
|
||||
if (!(sender instanceof ConsoleCommandSender)) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return false;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// 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) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return false;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// 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 Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
return true; // Valid
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(final CommandSender sender,
|
||||
public List<String> onTabComplete(final @NotNull CommandSender sender,
|
||||
final Command command,
|
||||
final String commandAlias,
|
||||
final String @NotNull [] args) {
|
||||
@@ -145,33 +52,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
if (args.length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (args.length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = args[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
return getCachedTabSuggestions(args);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,8 @@ dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
|
||||
implementation("com.velocitypowered:velocity-native:1.1.9") {
|
||||
exclude(group = "com.google.guava")
|
||||
exclude(group = "io.netty")
|
||||
exclude(group = "org.checkerframework")
|
||||
}
|
||||
|
||||
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
|
||||
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
|
||||
|
||||
@@ -18,34 +18,32 @@
|
||||
package xyz.jonesdev.sonar.bungee;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
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.BungeeInvocationSource;
|
||||
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
|
||||
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;
|
||||
import xyz.jonesdev.sonar.bungee.verbose.VerboseWrapper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), new VerboseWrapper(plugin.getServer()));
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for BungeeCord audiences
|
||||
*/
|
||||
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -56,34 +54,17 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.INFO, message, args);
|
||||
getPlugin().getLogger().info(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.WARNING, message, args);
|
||||
getPlugin().getLogger().warning(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.SEVERE, message, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUNGEE) {
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSource> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> player.getName().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(BungeeInvocationSource::new);
|
||||
getPlugin().getLogger().severe(buildFullMessage(message, args));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,25 +72,16 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue()::poll,
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Register traffic counter reset task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), TrafficCounter::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Register action bar verbose task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getVerboseHandler()::update,
|
||||
100L, 100L, TimeUnit.MILLISECONDS);
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -17,16 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
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));
|
||||
super(sender.getName(),
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
sender instanceof ProxiedPlayer,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+4
-128
@@ -17,31 +17,18 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
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.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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
|
||||
public BungeeSonarCommand() {
|
||||
super("sonar");
|
||||
cacheHelpMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,127 +36,16 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
|
||||
if (!(sender instanceof ConsoleCommandSender)) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// 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) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// 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 Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
}
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
|
||||
public Iterable<String> onTabComplete(final @NotNull CommandSender sender, final String @NotNull [] args) {
|
||||
// Do not allow tab completion if the player does not have the required permission
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
if (args.length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (args.length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = args[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
return getCachedTabSuggestions(args);
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.connection.DownstreamBridge;
|
||||
import net.md_5.bungee.connection.UpstreamBridge;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PacketHandler;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
private ChannelWrapper channel;
|
||||
private PacketHandler handler;
|
||||
|
||||
@Override
|
||||
public void setHandler(final @NotNull PacketHandler handler) {
|
||||
this.handler = handler;
|
||||
super.setHandler(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel = new ChannelWrapper(ctx);
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
handler.writabilityChanged(channel);
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
|
||||
try {
|
||||
handler.exception(throwable);
|
||||
} catch (Exception exception) {
|
||||
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
channel.markClosed();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* 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.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
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.EncryptionUtil;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.PlayerPublicKey;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import net.md_5.bungee.protocol.packet.StatusRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private final @NotNull BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper user;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
private boolean receivedStatusPacket;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new CorruptedFrameException("Duplicate status packet");
|
||||
}
|
||||
receivedStatusPacket = true;
|
||||
// Run the rest of the method asynchronously
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// The channel always stays open because the client sends
|
||||
// a StatusRequest and a Ping packet after one another
|
||||
if (!isConnected()) {
|
||||
throw new CorruptedFrameException("Not connected anymore");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
} catch (Exception exception) {
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new CorruptedFrameException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
final int protocolId = getHandshake().getProtocolVersion();
|
||||
protocolVersion = ProtocolVersion.fromId(protocolId);
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
try {
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
|
||||
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
user = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
|
||||
if (publicKey == null) {
|
||||
disconnect(bungee.getTranslation("secure_profile_required"));
|
||||
return;
|
||||
}
|
||||
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
|
||||
disconnect(bungee.getTranslation("secure_profile_expired"));
|
||||
return;
|
||||
}
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
&& !EncryptionUtil.check(publicKey, null)) {
|
||||
disconnect(bungee.getTranslation("secure_profile_invalid"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
|
||||
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
final int limit = bungee.config.getPlayerLimit();
|
||||
if (limit > 0 && bungee.getOnlineCount() >= limit) {
|
||||
disconnect(bungee.getTranslation("proxy_full"));
|
||||
return;
|
||||
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
return;
|
||||
}
|
||||
|
||||
// The player joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", loginRequest.getData())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| user.getPipeline().get(PACKET_DECODER) == null) {
|
||||
channelWrapper.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
user.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
|
||||
|
||||
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
|
||||
if (cachedKickPacket == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
|
||||
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
|
||||
}
|
||||
return cachedKickPacket;
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (channelWrapper.getHandle().isActive()) {
|
||||
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
channelWrapper.getHandle().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
channelWrapper.getHandle().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-92
@@ -17,113 +17,42 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
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.event.LoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.compress.PacketCompressor;
|
||||
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 xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
private final Fallback fallback;
|
||||
|
||||
private static final Field CHANNEL_WRAPPER;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_WRAPPER.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
|
||||
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
|
||||
// Try to close the channel with a custom serialized disconnect component
|
||||
if (pendingConnection instanceof FallbackInitialHandler) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
|
||||
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
fallbackInitialHandler.closeWith(new Kick(serialized));
|
||||
} else {
|
||||
// Fallback by disconnecting without a message
|
||||
pendingConnection.disconnect();
|
||||
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
|
||||
}
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
|
||||
.replace("%player%", event.getPlayer().getName())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
|
||||
}
|
||||
return;
|
||||
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdown().getNotification()));
|
||||
// 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().getMaxOnlinePerIp();
|
||||
|
||||
if (maxOnlinePerIp > 0) {
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
}
|
||||
}
|
||||
|
||||
final InitialHandler initialHandler = (InitialHandler) event.getPlayer().getPendingConnection();
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER.get(initialHandler);
|
||||
|
||||
final ChannelPipeline pipeline = channelWrapper.getHandle().pipeline();
|
||||
|
||||
// You don't need the frame decoder anymore
|
||||
if (pipeline.get(FRAME_PREPENDER) != null) {
|
||||
pipeline.remove(FRAME_PREPENDER);
|
||||
}
|
||||
|
||||
final int compressionThreshold = BungeeCord.getInstance().getConfig().getCompressionThreshold();
|
||||
|
||||
// Replace compression handlers
|
||||
if (compressionThreshold > 0
|
||||
&& pipeline.get(PacketCompressor.class) != null
|
||||
&& pipeline.get(PacketDecompressor.class) != null) {
|
||||
final VelocityCompressor compressor = Natives.compress.get().create(-1);
|
||||
|
||||
// Replace (de)compressor with Velocity's to ensure better performance
|
||||
pipeline.replace(
|
||||
PacketCompressor.class,
|
||||
"compress",
|
||||
// Create a new compressor instance with the Velocity compressor
|
||||
new FallbackPacketCompressor(compressionThreshold, compressor)
|
||||
);
|
||||
pipeline.replace(
|
||||
PacketDecompressor.class,
|
||||
"decompress",
|
||||
// Create a new decompressor instance with the Velocity decompressor
|
||||
new FallbackPacketDecompressor(compressionThreshold, compressor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
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.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.compress;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.compress.PacketCompressor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.zip.DataFormatException;
|
||||
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.*;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public final class FallbackPacketCompressor extends PacketCompressor {
|
||||
private int compressionThreshold;
|
||||
private final @NotNull VelocityCompressor compressor;
|
||||
|
||||
private static final int MAX_COMPRESSED_LENGTH = 1 << 21;
|
||||
|
||||
@Override
|
||||
public void setThreshold(final int compressionThreshold) {
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
|
||||
compressor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final ByteBuf out) throws Exception {
|
||||
final int uncompressed = msg.readableBytes();
|
||||
if (uncompressed < compressionThreshold) {
|
||||
// Under the threshold, do nothing.
|
||||
writeVarInt(out, uncompressed + 1);
|
||||
writeVarInt(out, 0);
|
||||
out.writeBytes(msg);
|
||||
} else {
|
||||
handleCompressed(ctx, msg, out);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCompressed(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final ByteBuf out) throws DataFormatException {
|
||||
final int uncompressed = msg.readableBytes();
|
||||
|
||||
write21BitVarInt(out, 0); // Fake packet length
|
||||
writeVarInt(out, uncompressed);
|
||||
|
||||
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, msg);
|
||||
|
||||
final int startCompressed = out.writerIndex();
|
||||
try {
|
||||
compressor.deflate(compatibleIn, out);
|
||||
} finally {
|
||||
compatibleIn.release();
|
||||
}
|
||||
|
||||
final int compressedLength = out.writerIndex() - startCompressed;
|
||||
if (compressedLength >= MAX_COMPRESSED_LENGTH) {
|
||||
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
|
||||
}
|
||||
|
||||
int writerIndex = out.writerIndex();
|
||||
int packetLength = out.readableBytes() - 3;
|
||||
out.writerIndex(0);
|
||||
write21BitVarInt(out, packetLength); // Rewrite packet length
|
||||
out.writerIndex(writerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuf allocateBuffer(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final boolean preferDirect) throws Exception {
|
||||
int uncompressed = msg.readableBytes();
|
||||
if (uncompressed < compressionThreshold) {
|
||||
int finalBufferSize = uncompressed + 1;
|
||||
finalBufferSize += varIntBytes(finalBufferSize);
|
||||
return ctx.alloc().directBuffer(finalBufferSize);
|
||||
}
|
||||
|
||||
final int initialBufferSize = (uncompressed - 1) + 3 + varIntBytes(uncompressed);
|
||||
return preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.compress;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.compress.PacketDecompressor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
@Getter
|
||||
public final class FallbackPacketDecompressor extends PacketDecompressor {
|
||||
private final int compressionThreshold;
|
||||
private final @NotNull VelocityCompressor compressor;
|
||||
|
||||
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
|
||||
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
|
||||
private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("sonar.increased-compression-cap")
|
||||
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
|
||||
|
||||
public FallbackPacketDecompressor(final int compressionThreshold,
|
||||
final @NotNull VelocityCompressor compressor) {
|
||||
super(); // super(compressionThreshold);
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
this.compressor = compressor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
|
||||
compressor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf in,
|
||||
final List<Object> out) throws Exception {
|
||||
final int claimedUncompressedSize = readVarInt(in);
|
||||
if (claimedUncompressedSize == 0) {
|
||||
// This message is not compressed.
|
||||
out.add(in.retain());
|
||||
|
||||
// Check for invalid packet size after the packet is retained
|
||||
final int size = in.readableBytes();
|
||||
if (size > compressionThreshold + 2) {
|
||||
throw new CorruptedFrameException("Invalid uncompressed packet size " + size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (claimedUncompressedSize < compressionThreshold) {
|
||||
throw new CorruptedFrameException(claimedUncompressedSize + " is less than " + compressionThreshold);
|
||||
}
|
||||
if (claimedUncompressedSize > UNCOMPRESSED_CAP) {
|
||||
throw new CorruptedFrameException(claimedUncompressedSize + " exceeds maximum size");
|
||||
}
|
||||
|
||||
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||
final ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
||||
|
||||
try {
|
||||
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
||||
out.add(uncompressed);
|
||||
} catch (Exception exception) {
|
||||
uncompressed.release();
|
||||
throw exception;
|
||||
} finally {
|
||||
compatibleIn.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.handler;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
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.FallbackUserWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
private ChannelWrapper channelWrapper;
|
||||
@SuppressWarnings("unused") // TODO: remove later
|
||||
private @Nullable FallbackUserWrapper player;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
|
||||
if (player == null && !Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
player = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress,
|
||||
ProtocolVersion.fromId(getHandshake().getProtocolVersion())
|
||||
);
|
||||
// TODO: implement Fallback
|
||||
}
|
||||
|
||||
super.handle(loginRequest);
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (player != null && player.getChannel().isActive()) {
|
||||
boolean is17 = player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
player.getChannel().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
player.getChannel().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-12
@@ -17,16 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
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.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
|
||||
@@ -41,15 +40,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
|
||||
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
|
||||
|
||||
private static final WriteBufferWaterMark MARK;
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
|
||||
|
||||
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field markField = PipelineUtils.class.getDeclaredField("MARK");
|
||||
markField.setAccessible(true);
|
||||
MARK = (WriteBufferWaterMark) markField.get(null);
|
||||
|
||||
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
|
||||
frameEncoder.setAccessible(true);
|
||||
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
|
||||
@@ -68,15 +65,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
}
|
||||
|
||||
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
|
||||
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
|
||||
channel.config().setWriteBufferWaterMark(MARK);
|
||||
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
|
||||
|
||||
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
|
||||
));
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
|
||||
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new HandlerBoss());
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -23,15 +23,15 @@ import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.event.ClientConnectEvent;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.SocketAddress;
|
||||
@@ -43,7 +43,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
|
||||
|
||||
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
|
||||
|
||||
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
|
||||
private static final KickStringWriter LEGACY_KICK;
|
||||
|
||||
static {
|
||||
@@ -63,7 +63,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
|
||||
: channel.remoteAddress();
|
||||
|
||||
if (BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle(remoteAddress)) {
|
||||
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
|
||||
channel.close();
|
||||
} else {
|
||||
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
|
||||
@@ -80,13 +80,12 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
}
|
||||
|
||||
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(Protocol.HANDSHAKE, true,
|
||||
ProxyServer.getInstance().getProtocolVersion()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(Protocol.HANDSHAKE,
|
||||
true, ProxyServer.getInstance().getProtocolVersion()));
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
|
||||
channel.pipeline().get(HandlerBoss.class).setHandler(new FallbackInitialHandler(BungeeCord.getInstance(),
|
||||
listener));
|
||||
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
|
||||
|
||||
if (listener.isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,7 +30,7 @@ import java.util.List;
|
||||
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx,
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final ByteBuf byteBuf,
|
||||
final List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.verbose;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class VerboseWrapper extends Verbose {
|
||||
private final ProxyServer server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final Component component) {
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
final ProxiedPlayer player = server.getPlayer(subscriber);
|
||||
if (player != null) {
|
||||
// TODO: use an adventure audience
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.SonarSupplier;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
@@ -32,6 +33,7 @@ import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.ServiceRepository;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
@@ -48,19 +50,21 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
private final SystemTimer launchTimer = new SystemTimer();
|
||||
|
||||
public SonarBootstrap(final @NotNull T plugin,
|
||||
final File dataDirectory,
|
||||
final Verbose verboseHandler) {
|
||||
// Set the API to this instance so the config doesn't have issues
|
||||
final SonarPlatform platform) {
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
|
||||
// Set the plugin instance before anything else
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.verboseHandler = verboseHandler;
|
||||
this.platform = platform;
|
||||
this.verboseHandler = new Verbose();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
}
|
||||
@@ -82,6 +86,9 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getLogger().info("Successfully initialized components in {}s!", launchTimer);
|
||||
getLogger().info("Enabling all tasks and features...");
|
||||
|
||||
// Start all service threads
|
||||
ServiceRepository.register();
|
||||
|
||||
try {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
@@ -126,7 +133,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
|
||||
public void shutdown() {
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// ...
|
||||
|
||||
// Shut down service threads
|
||||
ServiceRepository.shutdown();
|
||||
|
||||
getLogger().info("Successfully shut down. Goodbye!");
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -20,24 +20,23 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final String username;
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(final ChannelHandlerContext ctx) {
|
||||
final InetAddress inetAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
|
||||
|
||||
// Remove the IP address from the queue
|
||||
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
|
||||
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
|
||||
+6
-4
@@ -21,24 +21,26 @@ import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
private boolean closed;
|
||||
private boolean knownDisconnect;
|
||||
|
||||
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
|
||||
super(timeout, 0L, 0L, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx, final IdleStateEvent idleStateEvent) throws Exception {
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
assert idleStateEvent.state() == IdleState.READER_IDLE;
|
||||
readTimedOut(ctx);
|
||||
}
|
||||
|
||||
private void readTimedOut(final ChannelHandlerContext ctx) {
|
||||
if (!closed) {
|
||||
if (!knownDisconnect) {
|
||||
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
@@ -47,7 +49,7 @@ public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
closed = true;
|
||||
knownDisconnect = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+178
-77
@@ -20,20 +20,25 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Setter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
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.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -44,22 +49,28 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
// General
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private final @NotNull FallbackUser<?, ?> user;
|
||||
private final String username;
|
||||
private final UUID playerUuid;
|
||||
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
|
||||
|
||||
// Checks
|
||||
private short expectedTransactionId;
|
||||
private long expectedKeepAliveId;
|
||||
private int expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets;
|
||||
private int ignoredMovementTicks;
|
||||
private int expectedKeepAliveId, expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets, ignoredMovementTicks;
|
||||
private double posX, posY, posZ, lastY;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
@Setter
|
||||
private @NotNull State state = State.LOGIN_ACK;
|
||||
private boolean listenForMovements;
|
||||
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private static final Random RANDOM = new Random();
|
||||
// Map captcha
|
||||
private final SystemTimer keepAlive = new SystemTimer();
|
||||
private final SystemTimer actionBar = new SystemTimer();
|
||||
private @Nullable PreparedMapInfo captcha;
|
||||
private int captchaTriesLeft;
|
||||
|
||||
public enum State {
|
||||
// 1.20.2 configuration state
|
||||
@@ -69,7 +80,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// post-JOIN checks
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION, COLLISIONS,
|
||||
TELEPORT, POSITION,
|
||||
// Captcha
|
||||
MAP_CAPTCHA,
|
||||
// Done
|
||||
SUCCESS
|
||||
}
|
||||
@@ -90,8 +103,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
state = State.CONFIGURE;
|
||||
// Send necessary configuration packets to the client
|
||||
user.delayedWrite(synchronizeRegistry);
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(REGISTRY_SYNC);
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
@@ -142,11 +155,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
|
||||
if (!v1_20_2) {
|
||||
// Set the state to CLIENT_SETTINGS to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
// (1.20.2 needs the LOGIN_ACK state) and go on with
|
||||
// the flow of the verification.
|
||||
state = State.CLIENT_SETTINGS;
|
||||
}
|
||||
// Select the JoinGame packet for the respective protocol version
|
||||
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
|
||||
// Send the JoinGame packet so we can continue the verification
|
||||
user.write(joinGame);
|
||||
if (v1_20_2) {
|
||||
// Perform the transaction check since the ClientSettings
|
||||
// and PluginMessage packets have already been validated.
|
||||
@@ -165,23 +179,37 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Teleport the player to the spawn position
|
||||
user.delayedWrite(new PositionLook(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0f, 0f, expectedTeleportId, false
|
||||
));
|
||||
0f, -90f, expectedTeleportId, false));
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
user.delayedWrite(dynamicSpawnPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendChunkData() {
|
||||
// Set the state to POSITION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.POSITION;
|
||||
// 1.20.3 packets introduced game events
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
user.delayedWrite(START_WRITING_CHUNKS);
|
||||
}
|
||||
// Teleport player into the fake lobby by sending an empty chunk
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(UPDATE_SECTION_BLOCKS);
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
|
||||
// Switch to captcha state if needed
|
||||
captchaOrFinish();
|
||||
} else {
|
||||
// Make sure the player knows we are checking them
|
||||
user.delayedWrite(youAreBeingChecked);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
|
||||
@@ -218,13 +246,60 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified, drop all other packets
|
||||
if (state == State.SUCCESS) return;
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
// We are expecting a captcha code in chat, drop all other packets
|
||||
if (state == State.MAP_CAPTCHA) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof Chat) {
|
||||
final Chat chat = (Chat) packet;
|
||||
Objects.requireNonNull(captcha);
|
||||
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
return;
|
||||
}
|
||||
// Captcha is correct
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Every second
|
||||
if (actionBar.elapsed(1000L)) {
|
||||
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
|
||||
// Only send action bar if the message is actually supposed to be sent
|
||||
if (!actionBarMessage.isEmpty()) {
|
||||
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
|
||||
user.write(new Chat(MiniMessage.miniMessage().deserialize(
|
||||
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
|
||||
// Make sure to reset the timer
|
||||
actionBar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Every about 10 seconds
|
||||
if (keepAlive.elapsed(timeout)) {
|
||||
// Send the message again to remind the player
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
|
||||
// Send both packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Make sure to reset the timer
|
||||
keepAlive.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// Check if the time limit has exceeded
|
||||
if (login.elapsed(timeout)) {
|
||||
user.getChannel().close();
|
||||
@@ -323,18 +398,6 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Check if the transaction ID is valid
|
||||
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
|
||||
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, send an Abilities packet to the client to make
|
||||
// sure the player falls even in spectator mode.
|
||||
// Then, teleport the player to the spawn position.
|
||||
@@ -405,11 +468,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) <= BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) <= BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// The onGround property can never be true when we aren't checking for collisions
|
||||
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!listenForMovements) {
|
||||
@@ -427,44 +487,39 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Check for too many ignored Y ticks
|
||||
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks();
|
||||
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getGravity().getMaxIgnoredTicks();
|
||||
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double collisionOffsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
|
||||
|
||||
if (tick > maxMovementTick) {
|
||||
if (!Sonar.get().getConfig().getVerification().isCheckCollisions()) {
|
||||
// Checking collisions is disabled, just finish verification
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != State.COLLISIONS) {
|
||||
// Now we don't care about gravity anymore,
|
||||
// we just want the player to collide with the blocks.
|
||||
state = State.COLLISIONS;
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - offset: {}", username, x, y, z, offsetY);
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
|
||||
|
||||
// The player is colliding to blocks, finish verification
|
||||
if (ground) {
|
||||
// The player is colliding to blocks, finish verification
|
||||
finish();
|
||||
captchaOrFinish();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
}
|
||||
|
||||
if (!ground && tick < maxPredictionTick) {
|
||||
// Make sure we don't run out of predicted Y motions
|
||||
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
|
||||
|
||||
if (!ground) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
@@ -481,17 +536,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
tick++;
|
||||
}
|
||||
|
||||
private void assertState(final @NotNull State expectedState) {
|
||||
if (expectedState != state) {
|
||||
user.fail("expected " + expectedState + ", got " + state);
|
||||
private void captchaOrFinish() {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
|
||||
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
|
||||
state = State.MAP_CAPTCHA;
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
|
||||
}
|
||||
// Initialize the map captcha
|
||||
handleMapCaptcha();
|
||||
} else {
|
||||
// Finish the verification
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
if (!condition) {
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
private void handleMapCaptcha() {
|
||||
// Reset max tries
|
||||
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlot(0, 36, 1, 0,
|
||||
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
captcha = MapInfoPreparer.getRandomCaptcha();
|
||||
Objects.requireNonNull(captcha).write(user);
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(CAPTCHA_ABILITIES);
|
||||
// Make sure the player knows what to do
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
@@ -514,4 +593,26 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", login.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails the verification if a certain state is unexpected.
|
||||
*
|
||||
* @param expectedState Expected state
|
||||
*/
|
||||
private void assertState(final @NotNull State expectedState) {
|
||||
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a certain condition is met, fails the verification if not.
|
||||
*
|
||||
* @param condition Condition to fail if it's false
|
||||
* @param message Messages displayed in the stacktrace
|
||||
*/
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
if (!condition) {
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -33,14 +33,13 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackPacketListener listener;
|
||||
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
|
||||
final @NotNull FallbackPacketListener listener) {
|
||||
this.user = user;
|
||||
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
|
||||
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
|
||||
this.listener = listener;
|
||||
updateRegistry(user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
@@ -66,8 +65,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// If the packet hasn't been found, skip it
|
||||
// TODO: Can we fail the verification afterwards?
|
||||
// If the packet isn't found, skip it
|
||||
if (packet == null) {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
return;
|
||||
@@ -93,6 +91,9 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
|
||||
// Let our verification handler process the packet
|
||||
listener.handle(packet);
|
||||
|
||||
// Fire channel read to avoid timeout
|
||||
ctx.fireChannelRead(packet);
|
||||
} finally {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
|
||||
+88
-55
@@ -23,7 +23,7 @@ import lombok.Data;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
@@ -48,13 +48,13 @@ public enum FallbackPacketRegistry {
|
||||
CONFIG {
|
||||
{
|
||||
clientbound.register(Disconnect.class, Disconnect::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false));
|
||||
map(0x01, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(RegistrySync.class, RegistrySync::new,
|
||||
map(0x05, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(RegistryData.class, RegistryData::new,
|
||||
map(0x05, MINECRAFT_1_20_2, true));
|
||||
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x00, MINECRAFT_1_20_2, false));
|
||||
@@ -166,7 +166,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x40, MINECRAFT_1_19_1, true),
|
||||
map(0x3F, MINECRAFT_1_19_3, true),
|
||||
map(0x43, MINECRAFT_1_19_4, true),
|
||||
map(0x45, MINECRAFT_1_20_2, true));
|
||||
map(0x45, MINECRAFT_1_20_2, true),
|
||||
map(0x47, MINECRAFT_1_20_3, true));
|
||||
clientbound.register(Transaction.class, Transaction::new,
|
||||
map(0x32, MINECRAFT_1_7_2, true),
|
||||
map(0x11, MINECRAFT_1_9, true),
|
||||
@@ -180,19 +181,73 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2E, MINECRAFT_1_19_3, true),
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
map(0x45, MINECRAFT_1_12, true),
|
||||
map(0x46, MINECRAFT_1_12_1, true),
|
||||
map(0x49, MINECRAFT_1_13, true),
|
||||
map(0x4D, MINECRAFT_1_14, true),
|
||||
map(0x4E, MINECRAFT_1_15, true),
|
||||
map(0x42, MINECRAFT_1_16, true),
|
||||
map(0x4B, MINECRAFT_1_17, true),
|
||||
map(0x4A, MINECRAFT_1_19, true),
|
||||
map(0x4D, MINECRAFT_1_19_1, true),
|
||||
map(0x4C, MINECRAFT_1_19_3, true),
|
||||
map(0x50, MINECRAFT_1_19_4, true),
|
||||
map(0x52, MINECRAFT_1_20_2, true),
|
||||
map(0x54, MINECRAFT_1_20_3, true));
|
||||
clientbound.register(MapData.class, MapData::new,
|
||||
map(0x34, MINECRAFT_1_7_2, true),
|
||||
map(0x24, MINECRAFT_1_9, true),
|
||||
map(0x26, MINECRAFT_1_13, true),
|
||||
map(0x27, MINECRAFT_1_15, true),
|
||||
map(0x26, MINECRAFT_1_16, true),
|
||||
map(0x25, MINECRAFT_1_16_2, true),
|
||||
map(0x27, MINECRAFT_1_17, true),
|
||||
map(0x24, MINECRAFT_1_19, true),
|
||||
map(0x26, MINECRAFT_1_19_1, true),
|
||||
map(0x25, MINECRAFT_1_19_3, true),
|
||||
map(0x29, MINECRAFT_1_19_4, true),
|
||||
map(0x2A, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(SetSlot.class, SetSlot::new,
|
||||
map(0x2F, MINECRAFT_1_7_2, true),
|
||||
map(0x16, MINECRAFT_1_9, true),
|
||||
map(0x17, MINECRAFT_1_13, true),
|
||||
map(0x16, MINECRAFT_1_14, true),
|
||||
map(0x17, MINECRAFT_1_15, true),
|
||||
map(0x16, MINECRAFT_1_16, true),
|
||||
map(0x15, MINECRAFT_1_16_2, true),
|
||||
map(0x16, MINECRAFT_1_17, true),
|
||||
map(0x13, MINECRAFT_1_19, true),
|
||||
map(0x12, MINECRAFT_1_19_3, true),
|
||||
map(0x14, MINECRAFT_1_19_4, true),
|
||||
map(0x15, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(Chat.class, Chat::new,
|
||||
map(0x02, MINECRAFT_1_7_2, true),
|
||||
map(0x0F, MINECRAFT_1_9, true),
|
||||
map(0x0E, MINECRAFT_1_13, true),
|
||||
map(0x0F, MINECRAFT_1_15, true),
|
||||
map(0x0E, MINECRAFT_1_16, true),
|
||||
map(0x0F, MINECRAFT_1_18_2, true),
|
||||
map(0x5F, MINECRAFT_1_19, true),
|
||||
map(0x62, MINECRAFT_1_19_1, true),
|
||||
map(0x60, MINECRAFT_1_19_3, true),
|
||||
map(0x64, MINECRAFT_1_19_4, true),
|
||||
map(0x67, MINECRAFT_1_20_2, true));
|
||||
map(0x02, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_13, false),
|
||||
map(0x0F, MINECRAFT_1_15, false),
|
||||
map(0x0E, MINECRAFT_1_16, false),
|
||||
map(0x0F, MINECRAFT_1_17, false),
|
||||
map(0x5F, MINECRAFT_1_19, false),
|
||||
map(0x62, MINECRAFT_1_19_1, false),
|
||||
map(0x60, MINECRAFT_1_19_3, false),
|
||||
map(0x64, MINECRAFT_1_19_4, false),
|
||||
map(0x67, MINECRAFT_1_20_2, false),
|
||||
map(0x69, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(GameEvent.class, GameEvent::new,
|
||||
map(0x20, MINECRAFT_1_20_3, true));
|
||||
|
||||
serverbound.register(Chat.class, Chat::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
map(0x03, MINECRAFT_1_12, false),
|
||||
map(0x02, MINECRAFT_1_12_1, false),
|
||||
map(0x03, MINECRAFT_1_14, false),
|
||||
map(0x04, MINECRAFT_1_19, false),
|
||||
map(0x05, MINECRAFT_1_19_1, false),
|
||||
map(0x05, MINECRAFT_1_19_3, false));
|
||||
serverbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x0B, MINECRAFT_1_9, false),
|
||||
@@ -206,7 +261,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x12, MINECRAFT_1_19_1, false),
|
||||
map(0x11, MINECRAFT_1_19_3, false),
|
||||
map(0x12, MINECRAFT_1_19_4, false),
|
||||
map(0x14, MINECRAFT_1_20_2, false));
|
||||
map(0x14, MINECRAFT_1_20_2, false),
|
||||
map(0x15, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
@@ -244,7 +300,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x15, MINECRAFT_1_19_1, false),
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false));
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(Position.class, Position::new,
|
||||
@@ -260,7 +317,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x14, MINECRAFT_1_19_1, false),
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false));
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(Player.class, Player::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
@@ -274,7 +332,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x17, MINECRAFT_1_19_1, false),
|
||||
map(0x16, MINECRAFT_1_19_3, false),
|
||||
map(0x17, MINECRAFT_1_19_4, false),
|
||||
map(0x19, MINECRAFT_1_20_2, false));
|
||||
map(0x19, MINECRAFT_1_20_2, false),
|
||||
map(0x1A, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(Transaction.class, Transaction::new,
|
||||
map(0x0F, MINECRAFT_1_7_2, false),
|
||||
map(0x05, MINECRAFT_1_9, false),
|
||||
@@ -287,7 +346,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x20, MINECRAFT_1_19_4, false),
|
||||
map(0x23, MINECRAFT_1_20_2, false));
|
||||
map(0x23, MINECRAFT_1_20_2, false),
|
||||
map(0x24, MINECRAFT_1_20_3, false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,14 +368,13 @@ public enum FallbackPacketRegistry {
|
||||
private final Map<ProtocolVersion, ProtocolRegistry> versions;
|
||||
|
||||
PacketRegistry() {
|
||||
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
|
||||
for (ProtocolVersion version : ProtocolVersion.values()) {
|
||||
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
|
||||
for (final ProtocolVersion version : ProtocolVersion.values()) {
|
||||
if (!version.isLegacy() && !version.isUnknown()) {
|
||||
mutableVersions.put(version, new ProtocolRegistry(version));
|
||||
}
|
||||
}
|
||||
|
||||
versions = Collections.unmodifiableMap(mutableVersions);
|
||||
this.versions = Collections.unmodifiableMap(mutableVersions);
|
||||
}
|
||||
|
||||
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
|
||||
@@ -345,7 +404,7 @@ public enum FallbackPacketRegistry {
|
||||
break;
|
||||
}
|
||||
|
||||
final ProtocolRegistry registry = this.versions.get(protocol);
|
||||
final ProtocolRegistry registry = versions.get(protocol);
|
||||
if (registry == null) {
|
||||
throw new IllegalArgumentException("Unknown protocol version "
|
||||
+ current.protocolVersion);
|
||||
@@ -374,35 +433,12 @@ public enum FallbackPacketRegistry {
|
||||
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
|
||||
final @NotNull PacketMapping next,
|
||||
final @NotNull ProtocolVersion from) {
|
||||
final ProtocolVersion lastValid = current.lastValidProtocolVersion;
|
||||
|
||||
if (lastValid != null) {
|
||||
if (next != current) {
|
||||
throw new IllegalArgumentException("Cannot add a mapping after last valid mapping");
|
||||
}
|
||||
|
||||
if (from.compareTo(lastValid) > 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Last mapping version cannot be higher than highest mapping version");
|
||||
}
|
||||
}
|
||||
|
||||
final ProtocolVersion last = (ProtocolVersion) SUPPORTED_VERSIONS.toArray()[SUPPORTED_VERSIONS.size() - 1];
|
||||
final ProtocolVersion to = current == next ? lastValid != null
|
||||
? lastValid : last : next.protocolVersion;
|
||||
|
||||
final ProtocolVersion lastInList = lastValid != null ? lastValid : last;
|
||||
|
||||
if (from.compareTo(to) >= 0 && from != lastInList) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Next mapping version (%s) should be lower then current (%s)", to, from));
|
||||
}
|
||||
return to;
|
||||
return current == next ? LATEST_VERSION : next.protocolVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProtocolRegistry {
|
||||
public final ProtocolVersion version;
|
||||
private final ProtocolVersion version;
|
||||
private final IntObjectMap<Supplier<? extends FallbackPacket>> packetIdToSupplier =
|
||||
new IntObjectHashMap<>(16, 0.5f);
|
||||
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
|
||||
@@ -436,15 +472,12 @@ public enum FallbackPacketRegistry {
|
||||
private final int id;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final boolean encodeOnly;
|
||||
private final ProtocolVersion lastValidProtocolVersion;
|
||||
|
||||
PacketMapping(final int id,
|
||||
final ProtocolVersion protocolVersion,
|
||||
final ProtocolVersion lastValidProtocolVersion,
|
||||
final boolean packetDecoding) {
|
||||
this.id = id;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.lastValidProtocolVersion = lastValidProtocolVersion;
|
||||
this.encodeOnly = packetDecoding;
|
||||
}
|
||||
}
|
||||
@@ -452,6 +485,6 @@ public enum FallbackPacketRegistry {
|
||||
private static @NotNull PacketMapping map(final int id,
|
||||
final ProtocolVersion version,
|
||||
final boolean encodeOnly) {
|
||||
return new PacketMapping(id, version, null, encodeOnly);
|
||||
return new PacketMapping(id, version, encodeOnly);
|
||||
}
|
||||
}
|
||||
+64
-226
@@ -18,111 +18,87 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
|
||||
// Mappings
|
||||
private final CompoundBinaryTag CHAT_TYPE_119;
|
||||
private final CompoundBinaryTag CHAT_TYPE_1191;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_1194;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_120;
|
||||
|
||||
static {
|
||||
CHAT_TYPE_119 = getMapping("chat_1_19.nbt");
|
||||
CHAT_TYPE_1191 = getMapping("chat_1_19_1.nbt");
|
||||
DAMAGE_TYPE_1194 = getMapping("damage_1_19_4.nbt");
|
||||
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
|
||||
}
|
||||
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f);
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new Abilities(0x02, 0f, 0f);
|
||||
// Chunks
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
|
||||
// Registry Sync
|
||||
public FallbackPacket synchronizeRegistry;
|
||||
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket REGISTRY_SYNC = new RegistryData();
|
||||
// Keep Alive
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlive(0L);
|
||||
// Game Event (1.20.3+)
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new GameEvent(13, 0);
|
||||
// Chat
|
||||
public FallbackPacket enterCodeMessage;
|
||||
public FallbackPacket youAreBeingChecked;
|
||||
public FallbackPacket incorrectCaptcha;
|
||||
// JoinGame
|
||||
private FallbackPacket legacyJoinGame;
|
||||
private FallbackPacket joinGame116;
|
||||
private FallbackPacket joinGame1162;
|
||||
private FallbackPacket joinGame1182;
|
||||
private FallbackPacket joinGame1191;
|
||||
private FallbackPacket joinGame1194;
|
||||
private JoinGame joinGame120;
|
||||
|
||||
// Dimension
|
||||
private final DimensionInfo DIMENSION = new DimensionInfo(
|
||||
"minecraft:overworld", "sonar", false, false);
|
||||
public FallbackPacket joinGame;
|
||||
// Update Section Blocks
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
// Default Spawn Position
|
||||
public FallbackPacket dynamicSpawnPosition;
|
||||
|
||||
// Collisions
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
|
||||
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
|
||||
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
|
||||
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
|
||||
|
||||
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
|
||||
// Captcha position
|
||||
public final FallbackPacket CAPTCHA_POSITION = new PositionLook(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false);
|
||||
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION);
|
||||
|
||||
// Blocks
|
||||
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
|
||||
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
public double maxFallDistance;
|
||||
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
|
||||
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
|
||||
|
||||
public void prepare() {
|
||||
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
|
||||
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
|
||||
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
|
||||
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
|
||||
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
|
||||
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
|
||||
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
|
||||
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
|
||||
joinGame = new JoinGame(0,
|
||||
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
new String[]{"minecraft:overworld"},
|
||||
"minecraft:overworld");
|
||||
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
|
||||
maxPredictionTick = maxMovementTick + 10;
|
||||
preparedCachedYMotions = new double[maxPredictionTick + 1];
|
||||
|
||||
for (int i = 0; i < maxPredictionTick + 1; i++) {
|
||||
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
|
||||
}
|
||||
|
||||
// Adjust block and collide Y position based on max fall distance
|
||||
maxFallDistance = 0;
|
||||
for (int i = 0; i < maxMovementTick; i++) {
|
||||
maxFallDistance += preparedCachedYMotions[i];
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
|
||||
preparedCachedYMotions = new double[maxMovementTick + 8];
|
||||
|
||||
for (int i = 0; i < preparedCachedYMotions.length; i++) {
|
||||
final double gravity = -((Math.pow(0.98, i) - 1) * 3.92);
|
||||
preparedCachedYMotions[i] = gravity;
|
||||
maxFallDistance += gravity;
|
||||
}
|
||||
|
||||
// Set the dynamic spawn buffer
|
||||
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
|
||||
dynamicSpawnPosition = new DefaultSpawnPosition(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
|
||||
|
||||
// Prepare collision platform positions
|
||||
int index = 0;
|
||||
@@ -132,166 +108,28 @@ public class FallbackPreparer {
|
||||
x + (BLOCKS_PER_ROW / 2),
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0
|
||||
);
|
||||
0, 0);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare UpdateSectionBlocks packet
|
||||
UPDATE_SECTION_BLOCKS = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
|
||||
}
|
||||
updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
|
||||
|
||||
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return legacyJoinGame; // 1.7.2-1.15.2
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return joinGame116; // 1.16-1.16.1
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
|
||||
return joinGame1162; // 1.16.2-1.18
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
|
||||
return joinGame1182; // 1.18.1-1.19
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return joinGame1191; // 1.19.1-1.19.3
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
|
||||
return joinGame1194; // 1.19.4
|
||||
}
|
||||
return joinGame120; // 1.20-latest
|
||||
}
|
||||
|
||||
// Partially taken from
|
||||
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
|
||||
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
|
||||
final JoinGame joinGame = new JoinGame();
|
||||
|
||||
joinGame.setLevelType("flat");
|
||||
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
|
||||
joinGame.setDimension(0);
|
||||
joinGame.setReducedDebugInfo(true);
|
||||
|
||||
// 1.7.2-1.8.9 don't need any dimension information
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
|
||||
return joinGame;
|
||||
// "You are being checked" message
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
|
||||
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
|
||||
}
|
||||
|
||||
joinGame.setDimensionInfo(DIMENSION);
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
|
||||
enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode());
|
||||
incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha());
|
||||
|
||||
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
|
||||
final ListBinaryTag encodedDimensionRegistry = ListBinaryTag.builder(BinaryTagTypes.COMPOUND)
|
||||
.add(createDimensionData(protocolVersion))
|
||||
.build();
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
|
||||
|
||||
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
|
||||
dimensionRegistryEntry.put("value", encodedDimensionRegistry);
|
||||
|
||||
registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build());
|
||||
|
||||
final CompoundBinaryTag.Builder effectsTagBuilder = CompoundBinaryTag.builder()
|
||||
.putInt("sky_color", 7907327)
|
||||
.putInt("fog_color", 12638463)
|
||||
.putInt("water_color", 0)
|
||||
.putInt("water_fog_color", 0);
|
||||
|
||||
final CompoundBinaryTag.Builder elementTagBuilder = CompoundBinaryTag.builder()
|
||||
.putFloat("depth", 0.125f)
|
||||
.putFloat("temperature", 0.8f)
|
||||
.putFloat("scale", 0.05f)
|
||||
.putFloat("downfall", 0.4f)
|
||||
.putString("category", "plains")
|
||||
.put("effects", effectsTagBuilder.build());
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) >= 0) {
|
||||
elementTagBuilder.putBoolean("has_precipitation", false);
|
||||
} else {
|
||||
elementTagBuilder.putString("precipitation", "rain");
|
||||
}
|
||||
|
||||
registryContainer.put("minecraft:worldgen/biome", CompoundBinaryTag.builder()
|
||||
.putString("type", "minecraft:worldgen/biome")
|
||||
.put("value", ListBinaryTag.from(Collections.singletonList(
|
||||
CompoundBinaryTag.builder()
|
||||
.putString("name", "minecraft:plains")
|
||||
.putInt("id", 1)
|
||||
.put("element", elementTagBuilder.build())
|
||||
.build()
|
||||
))).build()
|
||||
);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) == 0) {
|
||||
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
|
||||
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
|
||||
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
|
||||
}
|
||||
} else {
|
||||
registryContainer.put("dimension", encodedDimensionRegistry);
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Precomputing map captcha answers...");
|
||||
// Precompute captcha answers
|
||||
MapInfoPreparer.prepare();
|
||||
Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}s!", timer);
|
||||
}
|
||||
|
||||
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
currentDimensionData = currentDimensionData.getCompound("element");
|
||||
}
|
||||
|
||||
joinGame.setCurrentDimensionData(currentDimensionData);
|
||||
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
|
||||
joinGame.setRegistry(registryContainer.build());
|
||||
return joinGame;
|
||||
}
|
||||
|
||||
public @NotNull CompoundBinaryTag createDimensionData(final @NotNull ProtocolVersion version) {
|
||||
final CompoundBinaryTag details = CompoundBinaryTag.builder()
|
||||
.putBoolean("natural", false)
|
||||
.putFloat("ambient_light", 0f)
|
||||
.putBoolean("shrunk", false)
|
||||
.putBoolean("ultrawarm", false)
|
||||
.putBoolean("has_ceiling", false)
|
||||
.putBoolean("has_skylight", true)
|
||||
.putBoolean("piglin_safe", false)
|
||||
.putBoolean("bed_works", false)
|
||||
.putBoolean("respawn_anchor_works", false)
|
||||
.putBoolean("has_raids", false)
|
||||
.putInt("logical_height", 256)
|
||||
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
|
||||
":infiniburn_nether" : "minecraft:infiniburn_nether")
|
||||
.putDouble("coordinate_scale", 1.0)
|
||||
.putString("effects", "minecraft:overworld")
|
||||
.putInt("min_y", 0)
|
||||
.putInt("height", 256)
|
||||
.putInt("monster_spawn_block_light_limit", 0)
|
||||
.putInt("monster_spawn_light_level", 0)
|
||||
.build();
|
||||
|
||||
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
return CompoundBinaryTag.builder()
|
||||
.putString("name", "minecraft:overworld")
|
||||
.putInt("id", 0)
|
||||
.put("element", details)
|
||||
.build();
|
||||
}
|
||||
|
||||
return details.putString("name", "minecraft:overworld");
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -28,7 +28,10 @@ import java.util.function.Function;
|
||||
public enum BlockType {
|
||||
STONE(protocolVersion -> 1),
|
||||
BARRIER(protocolVersion -> {
|
||||
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
|
||||
switch (protocolVersion) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
@@ -73,7 +76,8 @@ public enum BlockType {
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
|
||||
case MINECRAFT_1_20_3:
|
||||
return 10366;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+6
-9
@@ -24,18 +24,15 @@ import org.jetbrains.annotations.NotNull;
|
||||
public final class ChangedBlock {
|
||||
private final @NotNull BlockPosition position;
|
||||
private final BlockType type;
|
||||
private final int legacyChunkPosCrammed;
|
||||
private final int modernChunkPosCrammed;
|
||||
private final int legacyBlockState, blockState;
|
||||
|
||||
public ChangedBlock(final @NotNull BlockPosition position, final BlockType type) {
|
||||
this.position = position;
|
||||
this.type = type;
|
||||
this.legacyChunkPosCrammed = position.getX()
|
||||
- (position.getChunkX() << 4) << 12 | position.getZ()
|
||||
- (position.getChunkZ() << 4) << 8 | position.getY();
|
||||
this.modernChunkPosCrammed = position.getX()
|
||||
- (position.getChunkX() << 4) << 8 | position.getZ()
|
||||
- (position.getChunkZ() << 4) << 4 | position.getY()
|
||||
- ((position.getY() >> 4) << 4);
|
||||
final int x = position.getX() - (position.getChunkX() << 4);
|
||||
final int y = position.getY();
|
||||
final int z = position.getZ() - (position.getChunkZ() << 4);
|
||||
this.legacyBlockState = x << 12 | z << 8 | y;
|
||||
this.blockState = x << 8 | z << 4 | y - ((y >> 4) << 4);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -20,11 +20,13 @@ package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public final class DimensionInfo {
|
||||
private final String identifier, levelName;
|
||||
private final boolean flat, debug;
|
||||
private final String identifier;
|
||||
private final int id;
|
||||
private final CompoundBinaryTag tag;
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
@UtilityClass
|
||||
public final class DimensionRegistry {
|
||||
public final DimensionInfo DEFAULT_DIMENSION_1_16;
|
||||
public final DimensionInfo DEFAULT_DIMENSION_1_18_2;
|
||||
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_16;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_18_2;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_20;
|
||||
public final @NotNull CompoundBinaryTag OLD_CODEC;
|
||||
|
||||
static {
|
||||
CODEC_1_16 = getCodec("codec_1_16.nbt");
|
||||
CODEC_1_18_2 = getCodec("codec_1_18_2.nbt");
|
||||
CODEC_1_19 = getCodec("codec_1_19.nbt");
|
||||
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
|
||||
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
|
||||
CODEC_1_20 = getCodec("codec_1_20.nbt");
|
||||
OLD_CODEC = getCodec("codec_old.nbt");
|
||||
|
||||
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
|
||||
DEFAULT_DIMENSION_1_18_2 = getDimension(CODEC_1_18_2);
|
||||
}
|
||||
|
||||
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
|
||||
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
|
||||
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
|
||||
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
|
||||
}
|
||||
|
||||
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/codecs/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
throw new IllegalStateException(throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum ItemMapType {
|
||||
FILLED_MAP(protocolVersion -> {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
|
||||
switch (protocolVersion) {
|
||||
default:
|
||||
// 1.7-1.12.2
|
||||
return 358;
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 608;
|
||||
case MINECRAFT_1_13_2:
|
||||
return 613;
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 671;
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 733;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 847;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 886;
|
||||
case MINECRAFT_1_19_3:
|
||||
return 914;
|
||||
case MINECRAFT_1_19_4:
|
||||
return 937;
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
return 941;
|
||||
case MINECRAFT_1_20_3:
|
||||
return 979;
|
||||
}
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+8
-17
@@ -15,25 +15,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.verbose;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class VerboseWrapper extends Verbose {
|
||||
private final ProxyServer server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final Component component) {
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
server.getPlayer(subscriber).ifPresent(player -> {
|
||||
player.sendActionBar(component);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
public final class MapInfo {
|
||||
private final String answer;
|
||||
private final int columns, rows;
|
||||
private final int x, y;
|
||||
private final byte[] buffer;
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@UtilityClass
|
||||
public class MapInfoPreparer {
|
||||
private final Random RANDOM = new Random();
|
||||
|
||||
private final String[] FONT_TYPES = new String[]{
|
||||
Font.DIALOG_INPUT,
|
||||
Font.DIALOG,
|
||||
Font.SANS_SERIF,
|
||||
Font.SERIF
|
||||
};
|
||||
|
||||
private final int[] FONT_STYLES = new int[]{
|
||||
Font.PLAIN,
|
||||
Font.BOLD,
|
||||
Font.ITALIC,
|
||||
Font.ITALIC | Font.BOLD
|
||||
};
|
||||
|
||||
private final int[][] COLOR_PALETTE = new int[][]{
|
||||
new int[]{ // Blue
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51
|
||||
},
|
||||
new int[]{ // Black
|
||||
27,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
},
|
||||
new int[]{ // Green
|
||||
4,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
},
|
||||
new int[]{ // Red
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19
|
||||
}
|
||||
};
|
||||
|
||||
private PreparedMapInfo[] cached;
|
||||
|
||||
public void prepare() {
|
||||
cached = new PreparedMapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()];
|
||||
|
||||
final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary();
|
||||
|
||||
for (int i = 0; i < cached.length; i++) {
|
||||
// Create image
|
||||
final BufferedImage image = new BufferedImage(PreparedMapInfo.DIMENSIONS, PreparedMapInfo.DIMENSIONS,
|
||||
BufferedImage.TYPE_3BYTE_BGR);
|
||||
final Graphics2D graphics = image.createGraphics();
|
||||
|
||||
graphics.setColor(Color.WHITE);
|
||||
|
||||
// Create random font
|
||||
final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)];
|
||||
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
|
||||
final int fontSize = 30
|
||||
+ (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 5);
|
||||
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
|
||||
graphics.setFont(answerFont);
|
||||
|
||||
// Build answer to the captcha
|
||||
final StringBuilder answerBuilder = new StringBuilder();
|
||||
for (int _i = 0; _i < 4; _i++) {
|
||||
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
|
||||
}
|
||||
final String answer = answerBuilder.toString();
|
||||
|
||||
// Calculate text position
|
||||
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
|
||||
int _x = image.getWidth() / 2 - stringWidth / 2;
|
||||
int _y = image.getHeight() / 2 + fontSize / 3;
|
||||
int randomOffsetX = 0, randomOffsetY = 3;
|
||||
final int firstPixelX = _x, firstPixelY = _y;
|
||||
|
||||
// Draw each character one by one
|
||||
final FontMetrics fontMetrics = graphics.getFontMetrics();
|
||||
for (final char c : answer.toCharArray()) {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isRandomizePositions()) {
|
||||
_x += randomOffsetX;
|
||||
_y += randomOffsetY;
|
||||
randomOffsetX = randomOffsetX < 0 ? 1 + RANDOM.nextInt(2) : -1 - RANDOM.nextInt(2);
|
||||
randomOffsetY = randomOffsetY < 0 ? 1 + RANDOM.nextInt(4) : -1 - RANDOM.nextInt(4);
|
||||
}
|
||||
|
||||
final String character = String.valueOf(c);
|
||||
graphics.drawString(character, _x, _y);
|
||||
_x += fontMetrics.stringWidth(character);
|
||||
}
|
||||
|
||||
// Select random color palette
|
||||
final int[] colorPalette = COLOR_PALETTE[i % COLOR_PALETTE.length];
|
||||
// Calculate y and height
|
||||
final int pixelY = firstPixelY - fontSize;
|
||||
final int height = firstPixelY + fontSize / 3;
|
||||
// Store image in buffer
|
||||
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
|
||||
for (int x = firstPixelX; x < _x; x++) {
|
||||
for (int y = pixelY; y < height; y++) {
|
||||
final int index = y * image.getWidth() + x;
|
||||
final int pixel = image.getRGB(x, y);
|
||||
// If the pixel has no color set, set it to white/light gray (if enabled in the config)
|
||||
if (pixel == -16777216) {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isDrawBackgroundNoise()) {
|
||||
buffer[index] = (byte) (RANDOM.nextInt(100) < 70 ? 14 : 26);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Set color of pixel to random color from the palette
|
||||
buffer[index] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)];
|
||||
}
|
||||
}
|
||||
// Cache buffer to map
|
||||
cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public PreparedMapInfo getRandomCaptcha() {
|
||||
return cached[RANDOM.nextInt(cached.length)];
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapData;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
@Getter
|
||||
public final class PreparedMapInfo {
|
||||
public static final int DIMENSIONS = (int) Math.pow(2, 7);
|
||||
public static final int SCALE = DIMENSIONS * DIMENSIONS;
|
||||
|
||||
private final MapInfo info;
|
||||
private final MapData[] legacy;
|
||||
private final MapData modern;
|
||||
|
||||
public PreparedMapInfo(final String answer,
|
||||
final int columns, final int rows,
|
||||
final byte @NotNull [] buffer) {
|
||||
this.info = new MapInfo(answer, columns, rows, 0, 0, buffer);
|
||||
|
||||
// Prepare 1.7 map data using a grid
|
||||
final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS];
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
final byte buf = buffer[i];
|
||||
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
|
||||
}
|
||||
this.legacy = new MapData[grid.length];
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
this.legacy[i] = new MapData(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
|
||||
}
|
||||
|
||||
// Prepare 1.8+ map data
|
||||
this.modern = new MapData(info);
|
||||
}
|
||||
|
||||
public void write(final @NotNull FallbackUser<?, ?> user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
for (final MapData data : legacy) {
|
||||
user.delayedWrite(data);
|
||||
}
|
||||
} else {
|
||||
user.delayedWrite(modern);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-17
@@ -18,38 +18,26 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
|
||||
|
||||
import java.io.IOException;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class RegistrySync implements FallbackPacket {
|
||||
private CompoundBinaryTag dimensionRegistry;
|
||||
public final class RegistryData implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
|
||||
stream.writeByte(10); // CompoundTag ID
|
||||
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Cannot write NBT CompoundTag");
|
||||
}
|
||||
writeNamelessCompoundTag(byteBuf, DimensionRegistry.CODEC_1_20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -31,7 +31,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Abilities implements FallbackPacket {
|
||||
private byte encodedFlags;
|
||||
private int encodedFlags;
|
||||
private float flySpeed, walkSpeed;
|
||||
|
||||
@Override
|
||||
|
||||
+114
-19
@@ -17,48 +17,88 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.nbt.NBTMessageUtil.fromJson;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class Chat implements FallbackPacket {
|
||||
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
|
||||
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
|
||||
|
||||
public static final byte CHAT_TYPE = (byte) 0;
|
||||
public static final byte SYSTEM_TYPE = (byte) 1;
|
||||
public static final byte GAME_INFO_TYPE = (byte) 2;
|
||||
|
||||
private Component component;
|
||||
private byte position;
|
||||
private String message;
|
||||
private byte type;
|
||||
private boolean signedPreview;
|
||||
private boolean unsigned = false;
|
||||
private Instant timestamp;
|
||||
private Instant expiry;
|
||||
private long salt;
|
||||
private boolean signed;
|
||||
private byte[] signature;
|
||||
|
||||
// 1.20.3
|
||||
private BinaryTag nbtMessage;
|
||||
|
||||
// Clientbound LegacyChat
|
||||
public Chat(final Component component) {
|
||||
this(component, SYSTEM_TYPE);
|
||||
}
|
||||
|
||||
// Clientbound LegacyChat
|
||||
public Chat(final Component component, final byte type) {
|
||||
this(component, JSONComponentSerializer.json().serialize(component), type);
|
||||
}
|
||||
|
||||
public Chat(final Component component, final String message, final byte type) {
|
||||
this(component, message, type,
|
||||
false, false, null, null,
|
||||
0L, false, null, fromJson(new JsonParser().parse(message)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
writeString(byteBuf, serialized);
|
||||
|
||||
// Message type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(position == 2);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, position);
|
||||
// Serialized message
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, nbtMessage);
|
||||
} else {
|
||||
byteBuf.writeByte(this.position);
|
||||
writeString(byteBuf, message);
|
||||
}
|
||||
|
||||
// UUID
|
||||
// Type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(type == GAME_INFO_TYPE);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, type);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeByte(type);
|
||||
}
|
||||
|
||||
// Sender
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
writeUUID(byteBuf, PLACEHOLDER_UUID);
|
||||
@@ -66,7 +106,62 @@ public final class Chat implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
message = readString(byteBuf, 256);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) {
|
||||
final long expiresAt = byteBuf.readLong();
|
||||
final long saltLong = byteBuf.readLong();
|
||||
final byte[] signatureBytes = readByteArray(byteBuf);
|
||||
|
||||
if (saltLong != 0L && signatureBytes.length > 0) {
|
||||
signature = signatureBytes;
|
||||
expiry = Instant.ofEpochMilli(expiresAt);
|
||||
} else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0
|
||||
|| saltLong == 0L) && signatureBytes.length == 0) {
|
||||
unsigned = true;
|
||||
} else {
|
||||
throw new CorruptedFrameException("Invalid signature");
|
||||
}
|
||||
|
||||
signedPreview = byteBuf.readBoolean();
|
||||
if (signedPreview && unsigned) {
|
||||
throw new CorruptedFrameException("Signature missing");
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
|
||||
final int size = readVarInt(byteBuf);
|
||||
if (size < 0 || size > 5) {
|
||||
throw new CorruptedFrameException("Invalid previous messages");
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
readUUID(byteBuf);
|
||||
readByteArray(byteBuf);
|
||||
}
|
||||
|
||||
if (byteBuf.readBoolean()) {
|
||||
readUUID(byteBuf);
|
||||
readByteArray(byteBuf);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timestamp = Instant.ofEpochMilli(byteBuf.readLong());
|
||||
salt = byteBuf.readLong();
|
||||
signed = byteBuf.readBoolean();
|
||||
if (signed) {
|
||||
byte[] sign = new byte[256];
|
||||
byteBuf.readBytes(sign);
|
||||
signature = sign;
|
||||
} else {
|
||||
signature = new byte[0];
|
||||
}
|
||||
|
||||
readVarInt(byteBuf);
|
||||
byte[] bytes = new byte[DIV_FLOOR];
|
||||
byteBuf.readBytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class DefaultSpawnPosition implements FallbackPacket {
|
||||
private int x, y, z;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
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);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
byteBuf.writeFloat(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -17,11 +17,13 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -29,8 +31,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.nbt.NBTMessageUtil.fromJson;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
|
||||
@Getter
|
||||
@@ -39,10 +41,15 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
|
||||
@AllArgsConstructor
|
||||
public final class Disconnect implements FallbackPacket {
|
||||
private @Nullable String reason;
|
||||
private BinaryTag binaryTag;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
writeString(byteBuf, Objects.requireNonNull(reason));
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, binaryTag);
|
||||
} else {
|
||||
writeString(byteBuf, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,6 +58,8 @@ public final class Disconnect implements FallbackPacket {
|
||||
}
|
||||
|
||||
public static @NotNull Disconnect create(final Component component) {
|
||||
return new Disconnect(JSONComponentSerializer.json().serialize(component));
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
final BinaryTag binaryTag = fromJson(new JsonParser().parse(serialized));
|
||||
return new Disconnect(serialized, binaryTag);
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -29,14 +29,13 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString(of = {"x", "z"})
|
||||
public final class EmptyChunkData implements FallbackPacket {
|
||||
private int x, z;
|
||||
|
||||
@@ -92,7 +91,11 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
|
||||
writeCompoundTag(byteBuf, protocolVersion.compareTo(MINECRAFT_1_18) < 0 ? LEGACY_TAG : MODERN_TAG);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, MODERN_TAG);
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, protocolVersion.compareTo(MINECRAFT_1_18) < 0 ? LEGACY_TAG : MODERN_TAG);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0 && protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class GameEvent implements FallbackPacket {
|
||||
private int type;
|
||||
private float value;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(type);
|
||||
byteBuf.writeFloat(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+163
-126
@@ -21,167 +21,204 @@ import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class JoinGame implements FallbackPacket {
|
||||
private int entityId;
|
||||
private short gamemode;
|
||||
private int dimension;
|
||||
private long partialHashedSeed; // 1.15+
|
||||
private short difficulty;
|
||||
private int gamemode;
|
||||
private long partialHashedSeed;
|
||||
private boolean isHardcore;
|
||||
private @Nullable String levelType;
|
||||
private int viewDistance; // 1.14+
|
||||
private int viewDistance;
|
||||
private boolean reducedDebugInfo;
|
||||
private boolean showRespawnScreen;
|
||||
private boolean limitedCrafting; // 1.20.2+
|
||||
private String[] levelNames; // 1.16+
|
||||
private CompoundBinaryTag registry; // 1.16+
|
||||
private DimensionInfo dimensionInfo; // 1.16+
|
||||
private CompoundBinaryTag currentDimensionData; // 1.16.2+
|
||||
private short previousGamemode; // 1.16+
|
||||
private int simulationDistance; // 1.18+
|
||||
private boolean limitedCrafting;
|
||||
private String[] levelNames;
|
||||
private String levelName;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
|
||||
// haha funny, they made 1.20.2 more complicated
|
||||
encode1202Up(byteBuf, protocolVersion);
|
||||
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
// Minecraft 1.16 and above have significantly more complicated logic for reading this packet,
|
||||
// so separate it out.
|
||||
encode116Up(byteBuf, protocolVersion);
|
||||
} else {
|
||||
encodeLegacy(byteBuf, protocolVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeLegacy(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_7_2, MINECRAFT_1_7_6)) {
|
||||
byteBuf.writeByte(gamemode == 3 ? 1 : gamemode);
|
||||
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
} else {
|
||||
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) {
|
||||
byteBuf.writeInt(dimension);
|
||||
} else {
|
||||
byteBuf.writeByte(dimension);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
|
||||
byteBuf.writeByte(difficulty);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
}
|
||||
byteBuf.writeByte(1); // max players
|
||||
if (levelType == null) {
|
||||
throw new IllegalStateException("No level type specified.");
|
||||
}
|
||||
writeString(byteBuf, levelType);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_14, MINECRAFT_1_14_4)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
}
|
||||
}
|
||||
|
||||
private void encode116Up(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, OLD_CODEC);
|
||||
writeString(byteBuf, DEFAULT_DIMENSION_1_16.getIdentifier());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
} else {
|
||||
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
|
||||
}
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, registry);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
writeCompoundTag(byteBuf, currentDimensionData);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
} else {
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
}
|
||||
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
} else {
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_18) >= 0) {
|
||||
writeVarInt(byteBuf, simulationDistance);
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
writeCompoundTag(byteBuf, CODEC_1_18_2);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_18_2.getTag());
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
|
||||
}
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
// optional death location
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
byteBuf.writeBoolean(false); // no last death position
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_19, MINECRAFT_1_19_4)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) >= 0) {
|
||||
writeCompoundTag(byteBuf, CODEC_1_19_4);
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, CODEC_1_19_1);
|
||||
}
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, CODEC_1_19);
|
||||
}
|
||||
writeString(byteBuf, levelName); // world type
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
if (protocolVersion.equals(MINECRAFT_1_20)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, CODEC_1_20);
|
||||
writeString(byteBuf, levelName); // world type
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void encode1202Up(final @NotNull ByteBuf byteBuf,
|
||||
final @SuppressWarnings("unused") ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, simulationDistance);
|
||||
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(limitedCrafting);
|
||||
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
byteBuf.writeBoolean(false); // no last death position
|
||||
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // limited crafting
|
||||
writeString(byteBuf, levelName);
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class MapData implements FallbackPacket {
|
||||
private MapInfo mapInfo;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(mapInfo.getBuffer().length + 3);
|
||||
byteBuf.writeByte(0); // scaling
|
||||
byteBuf.writeByte(mapInfo.getX());
|
||||
byteBuf.writeByte(mapInfo.getY());
|
||||
|
||||
byteBuf.writeBytes(mapInfo.getBuffer());
|
||||
} else {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
} else {
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
|
||||
byteBuf.writeByte(mapInfo.getColumns());
|
||||
byteBuf.writeByte(mapInfo.getRows());
|
||||
byteBuf.writeByte(mapInfo.getX());
|
||||
byteBuf.writeByte(mapInfo.getY());
|
||||
|
||||
writeVarInt(byteBuf, mapInfo.getBuffer().length);
|
||||
byteBuf.writeBytes(mapInfo.getBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.IntBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetSlot implements FallbackPacket {
|
||||
private int windowId;
|
||||
private int slot;
|
||||
private int count;
|
||||
private int data;
|
||||
private int itemId;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(windowId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
|
||||
byteBuf.writeShort(slot);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) {
|
||||
byteBuf.writeBoolean(true);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
|
||||
byteBuf.writeShort(itemId);
|
||||
} else {
|
||||
writeVarInt(byteBuf, itemId);
|
||||
}
|
||||
byteBuf.writeByte(count);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
byteBuf.writeShort(data);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(-1);
|
||||
} else {
|
||||
byteBuf.writeByte(0);
|
||||
}
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, compoundBinaryTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -53,12 +53,12 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
}
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
|
||||
final int id = block.getType().getId(protocolVersion);
|
||||
byteBuf.writeShort(block.getLegacyBlockState());
|
||||
final int blockId = block.getType().getId(protocolVersion);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
writeVarInt(byteBuf, id);
|
||||
writeVarInt(byteBuf, blockId);
|
||||
} else {
|
||||
final int shiftedBlockId = id << 4;
|
||||
final int shiftedBlockId = blockId << 4;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(shiftedBlockId);
|
||||
} else {
|
||||
@@ -67,8 +67,10 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We only need one Y position
|
||||
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
|
||||
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
|
||||
|
||||
// 1.20+ don't have light update suppression
|
||||
@@ -79,8 +81,9 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
writeVarInt(byteBuf, changedBlocks.length);
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
|
||||
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
|
||||
final long positionIdValue = shiftedBlockId | block.getBlockState();
|
||||
writeVarLong(byteBuf, positionIdValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@UtilityClass
|
||||
public class ServiceRepository {
|
||||
private final ScheduledExecutorService QUEUE_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService TRAFFIC_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService VERBOSE_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private boolean registered;
|
||||
|
||||
public void register() {
|
||||
if (registered) {
|
||||
throw new IllegalStateException("Already registered");
|
||||
}
|
||||
registered = true;
|
||||
|
||||
TRAFFIC_SERVICE.scheduleAtFixedRate(TrafficCounter::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
QUEUE_SERVICE.scheduleAtFixedRate(Sonar.get().getFallback().getQueue()::poll,
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
VERBOSE_SERVICE.scheduleAtFixedRate(Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
if (!registered) {
|
||||
throw new IllegalStateException("Not registered");
|
||||
}
|
||||
registered = false;
|
||||
|
||||
QUEUE_SERVICE.shutdown();
|
||||
TRAFFIC_SERVICE.shutdown();
|
||||
VERBOSE_SERVICE.shutdown();
|
||||
}
|
||||
}
|
||||
-1
@@ -26,7 +26,6 @@ public final class SubcommandRegistryHolder implements SubcommandRegistry {
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
new StatisticsCommand(),
|
||||
new LockdownCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand()
|
||||
|
||||
+5
-5
@@ -39,7 +39,7 @@ import java.net.InetAddress;
|
||||
public final class BlacklistCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
@@ -54,7 +54,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
|
||||
if (SONAR.getFallback().getBlacklisted().has(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
|
||||
SONAR.getFallback().getBlacklisted().put(inetAddress);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
@@ -85,13 +85,13 @@ public final class BlacklistCommand extends Subcommand {
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
|
||||
if (!SONAR.getFallback().getBlacklisted().has(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
|
||||
SONAR.getFallback().getBlacklisted().invalidate(inetAddress);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
|
||||
+5
-9
@@ -17,12 +17,11 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
@@ -33,7 +32,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
// This command is only used to display helpful information in order to help
|
||||
// This command is only used to display helpful information to help
|
||||
// the Sonar contributors to fix issues more quickly
|
||||
@SubcommandInfo(
|
||||
name = "dump",
|
||||
@@ -41,13 +40,10 @@ import java.util.WeakHashMap;
|
||||
onlyConsole = true
|
||||
)
|
||||
public final class DumpCommand extends Subcommand implements JVMProfiler {
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = GSON.toJson(collectMappedInformation());
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
|
||||
SONAR.getLogger().info("Generated dump: {}", json);
|
||||
}
|
||||
|
||||
@@ -56,7 +52,7 @@ public final class DumpCommand extends Subcommand implements JVMProfiler {
|
||||
val mappings = new WeakHashMap<String, Object>();
|
||||
mappings.put("sonar", new Dump.Sonar(
|
||||
SONAR.getVersion().getFull(),
|
||||
SONAR.getServer().getPlatform(),
|
||||
SONAR.getPlatform(),
|
||||
SONAR.getVersion().isOnMainBranch()
|
||||
));
|
||||
mappings.put("runtime", new Dump.Runtime(
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
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;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "lockdown",
|
||||
description = "Prevent all players from joining"
|
||||
)
|
||||
public final class LockdownCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
|
||||
SONAR.getConfig().getLockdown().setEnabled(newState);
|
||||
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", newState);
|
||||
SONAR.getConfig().getGeneralConfig().save();
|
||||
|
||||
invocation.getSender().sendMessage(newState
|
||||
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
public final class ReloadCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
StatisticType type = StatisticType.GENERAL;
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
try {
|
||||
|
||||
+10
-26
@@ -17,46 +17,30 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verbose",
|
||||
description = "Enable and disable Sonar verbose",
|
||||
description = "Enable or disable Sonar verbose",
|
||||
onlyPlayers = true
|
||||
)
|
||||
public final class VerboseCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
InvocationSource verboseSubscriber = invocation.getSender();
|
||||
|
||||
// Support for '/sonar verbose [username]'
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
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().getCommands().getVerboseUnsubscribedOther()
|
||||
.replace("%player%", verboseSubscriber.getName()));
|
||||
}
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
|
||||
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
|
||||
// Reset ActionBar component when unsubscribing
|
||||
invocation.getSender().getAudience().sendActionBar(Component.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
if (verboseSubscriber != invocation.getSender()) {
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribedOther()
|
||||
.replace("%player%", verboseSubscriber.getName()));
|
||||
}
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
|
||||
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
|
||||
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -43,7 +43,7 @@ public final class VerifiedCommand extends Subcommand {
|
||||
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.update;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -57,7 +74,7 @@ public class UpdateChecker {
|
||||
try {
|
||||
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
|
||||
final HttpsURLConnection urlConnection = prepareConnection(url);
|
||||
final JsonObject json = parseJson(urlConnection.getInputStream());
|
||||
final JsonObject json = parseJson(urlConnection);
|
||||
final String latestStableRelease = json.get("tag_name").getAsString();
|
||||
final int convertedLatestVersion = convertVersion(latestStableRelease);
|
||||
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
|
||||
@@ -78,22 +95,25 @@ public class UpdateChecker {
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull JsonObject parseJson(final @NotNull InputStream inputStream) throws IOException {
|
||||
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
|
||||
final StringBuilder content = new StringBuilder();
|
||||
private @NotNull JsonObject parseJson(final @NotNull HttpsURLConnection urlConnection) throws IOException {
|
||||
try (final InputStream inputStream = urlConnection.getInputStream()) {
|
||||
try (final InputStreamReader inputStreamReader = new InputStreamReader(inputStream)) {
|
||||
final StringBuilder content = new StringBuilder();
|
||||
|
||||
// Read the entire page content
|
||||
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
String line;
|
||||
// Read the entire page content
|
||||
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
String line;
|
||||
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
content.append(line);
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
content.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(content.toString()).getAsJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(content.toString()).getAsJsonObject();
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
|
||||
|
||||
+11
-10
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.common.utility.geyser;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -27,20 +28,20 @@ import org.jetbrains.annotations.NotNull;
|
||||
@UtilityClass
|
||||
public class GeyserUtil {
|
||||
|
||||
/*
|
||||
* Geyser attribute key for every floodgate player
|
||||
*/
|
||||
private AttributeKey<Object> playerAttribute() {
|
||||
return AttributeKey.valueOf("floodgate-player");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Geyser holds the parent channel of the player
|
||||
*
|
||||
* @param channel Channel of the player
|
||||
* @return Whether the player is on GeyserMC or not
|
||||
*/
|
||||
public boolean isGeyserConnection(final @NotNull Channel channel) {
|
||||
// Get the parent channel of the original channel
|
||||
final Channel parent = channel.parent();
|
||||
// This shouldn't happen, but we want to stay safe here
|
||||
if (parent == null) return false;
|
||||
|
||||
final Class<? extends Channel> clazz = parent.getClass();
|
||||
// Check if Geyser adapted the channel by checking for the package name
|
||||
return clazz.getCanonicalName().startsWith("org.geysermc");
|
||||
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
|
||||
// Every floodgate player has a channel attribute called "floodgate-player"
|
||||
return channel.attr(playerAttribute()).get() != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.utility.nbt;
|
||||
|
||||
import com.google.gson.*;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-0e355ffd4eb0e15e2c2e628227debc9b6ccefeb167d7b9619311cdc7db2645ec
|
||||
@UtilityClass
|
||||
public class NBTMessageUtil {
|
||||
public static BinaryTag fromJson(final JsonElement json) {
|
||||
if (json instanceof JsonPrimitive) {
|
||||
JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
|
||||
if (jsonPrimitive.isNumber()) {
|
||||
Number number = json.getAsNumber();
|
||||
|
||||
if (number instanceof Byte) {
|
||||
return ByteBinaryTag.byteBinaryTag((Byte) number);
|
||||
} else if (number instanceof Short) {
|
||||
return ShortBinaryTag.shortBinaryTag((Short) number);
|
||||
} else if (number instanceof Integer) {
|
||||
return IntBinaryTag.intBinaryTag((Integer) number);
|
||||
} else if (number instanceof Long) {
|
||||
return LongBinaryTag.longBinaryTag((Long) number);
|
||||
} else if (number instanceof Float) {
|
||||
return FloatBinaryTag.floatBinaryTag((Float) number);
|
||||
} else if (number instanceof Double) {
|
||||
return DoubleBinaryTag.doubleBinaryTag((Double) number);
|
||||
}
|
||||
} else if (jsonPrimitive.isString()) {
|
||||
return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString());
|
||||
} else if (jsonPrimitive.isBoolean()) {
|
||||
return ByteBinaryTag.byteBinaryTag(jsonPrimitive.getAsBoolean() ? (byte) 1 : (byte) 0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive);
|
||||
}
|
||||
} else if (json instanceof JsonObject) {
|
||||
final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder();
|
||||
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
|
||||
builder.put(property.getKey(), fromJson(property.getValue()));
|
||||
}
|
||||
return builder.build();
|
||||
} else if (json instanceof JsonArray) {
|
||||
final JsonArray jsonArray = ((JsonArray) json);
|
||||
|
||||
if (jsonArray.size() == 0) {
|
||||
return ListBinaryTag.listBinaryTag(EndBinaryTag.endBinaryTag().type(), Collections.emptyList());
|
||||
}
|
||||
|
||||
final BinaryTagType<?> tagByteType = ByteBinaryTag.ZERO.type();
|
||||
final BinaryTagType<?> tagIntType = IntBinaryTag.intBinaryTag(0).type();
|
||||
final BinaryTagType<?> tagLongType = LongBinaryTag.longBinaryTag(0).type();
|
||||
|
||||
final BinaryTag listTag;
|
||||
final BinaryTagType<?> listType = fromJson(jsonArray.get(0)).type();
|
||||
|
||||
if (listType.equals(tagByteType)) {
|
||||
byte[] bytes = new byte[jsonArray.size()];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (Byte) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
listTag = ByteArrayBinaryTag.byteArrayBinaryTag(bytes);
|
||||
} else if (listType.equals(tagIntType)) {
|
||||
int[] ints = new int[jsonArray.size()];
|
||||
for (int i = 0; i < ints.length; i++) {
|
||||
ints[i] = (Integer) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
listTag = IntArrayBinaryTag.intArrayBinaryTag(ints);
|
||||
} else if (listType.equals(tagLongType)) {
|
||||
long[] longs = new long[jsonArray.size()];
|
||||
for (int i = 0; i < longs.length; i++) {
|
||||
longs[i] = (Long) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
listTag = LongArrayBinaryTag.longArrayBinaryTag(longs);
|
||||
} else {
|
||||
final List<BinaryTag> tagItems = new ArrayList<>(jsonArray.size());
|
||||
|
||||
for (final JsonElement jsonEl : jsonArray) {
|
||||
final BinaryTag subTag = fromJson(jsonEl);
|
||||
if (subTag.type() != listType) {
|
||||
throw new IllegalArgumentException("Cannot convert mixed JsonArray to Tag");
|
||||
}
|
||||
tagItems.add(subTag);
|
||||
}
|
||||
listTag = ListBinaryTag.listBinaryTag(listType, tagItems);
|
||||
}
|
||||
return listTag;
|
||||
} else if (json instanceof JsonNull) {
|
||||
return EndBinaryTag.endBinaryTag();
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown JSON element: " + json);
|
||||
}
|
||||
}
|
||||
+61
-3
@@ -24,8 +24,7 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.DataOutput;
|
||||
@@ -34,6 +33,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
|
||||
@UtilityClass
|
||||
@@ -48,6 +49,24 @@ public class ProtocolUtil {
|
||||
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
|
||||
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
|
||||
|
||||
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
|
||||
return new UUID(byteBuf.readLong(), byteBuf.readLong());
|
||||
}
|
||||
|
||||
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf) {
|
||||
return readByteArray(byteBuf, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) {
|
||||
int length = readVarInt(byteBuf);
|
||||
checkFrame(length >= 0, "Got a negative-length array");
|
||||
checkFrame(length <= cap, "Bad array size");
|
||||
checkFrame(byteBuf.isReadable(length), "Trying to read an array that is too long");
|
||||
byte[] array = new byte[length];
|
||||
byteBuf.readBytes(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
|
||||
final ByteBuf slice = content.slice();
|
||||
try {
|
||||
@@ -113,7 +132,46 @@ public class ProtocolUtil {
|
||||
public static void writeCompoundTag(final ByteBuf byteBuf, final CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
|
||||
} catch (IOException e) {
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Unable to encode NBT CompoundTag");
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-4aa8208044741102c6326c7e85086e6fa8fcc7c064f7df6fd0411baf5f2b4504
|
||||
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull BinaryTag binaryTag) {
|
||||
try (final ByteBufOutputStream output = new ByteBufOutputStream(byteBuf)) {
|
||||
// TODO: Find a way to improve this...
|
||||
output.writeByte(binaryTag.type().id());
|
||||
if (binaryTag instanceof CompoundBinaryTag) {
|
||||
CompoundBinaryTag tag = (CompoundBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof ByteBinaryTag) {
|
||||
ByteBinaryTag tag = (ByteBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof ShortBinaryTag) {
|
||||
ShortBinaryTag tag = (ShortBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof IntBinaryTag) {
|
||||
IntBinaryTag tag = (IntBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof LongBinaryTag) {
|
||||
LongBinaryTag tag = (LongBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof DoubleBinaryTag) {
|
||||
DoubleBinaryTag tag = (DoubleBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof StringBinaryTag) {
|
||||
StringBinaryTag tag = (StringBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof ListBinaryTag) {
|
||||
ListBinaryTag tag = (ListBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof EndBinaryTag) {
|
||||
EndBinaryTag tag = (EndBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Unable to encode NBT CompoundTag");
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user