mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b1bfd958 | ||
|
|
d97f124201 | ||
|
|
5e9d8b4b29 | ||
|
|
c3eabd1265 | ||
|
|
9d2752dc32 | ||
|
|
5fe9c18268 | ||
|
|
f93dae9532 | ||
|
|
ab36823ca2 | ||
|
|
5c94f06510 | ||
|
|
d069b9aadb | ||
|
|
3337009fe4 | ||
|
|
b010e18552 | ||
|
|
82b4b3bac7 | ||
|
|
b143ea548a | ||
|
|
689e3ea36b | ||
|
|
c9c1a2abb7 | ||
|
|
a8aa284a5e | ||
|
|
448f8d9af3 | ||
|
|
3d8f61fce3 | ||
|
|
cf27a9fe84 | ||
|
|
bec6251b2a | ||
|
|
6497b89f10 | ||
|
|
525d67808e | ||
|
|
8864c0749a | ||
|
|
1ec4704f8e | ||
|
|
8a4937e3af | ||
|
|
f90446d4e3 | ||
|
|
66141815cb | ||
|
|
3fe7ccd65c | ||
|
|
f6203416d1 | ||
|
|
3c73ecf60f | ||
|
|
c116d7b556 | ||
|
|
057cda6c7a | ||
|
|
3a97a1f949 | ||
|
|
69b20769de | ||
|
|
9b87fde7c6 | ||
|
|
b939767ab9 | ||
|
|
d5a8cd5346 | ||
|
|
aa4e4662bb | ||
|
|
29187d4248 | ||
|
|
0c8106eaf6 | ||
|
|
92ac5fbeb2 | ||
|
|
b800a2d154 | ||
|
|
1957463b81 | ||
|
|
25114eabe5 | ||
|
|
a45b526ffb | ||
|
|
7b70d32e84 | ||
|
|
e84c1a54db | ||
|
|
fd869ac199 | ||
|
|
abf1479a84 | ||
|
|
c00392e062 | ||
|
|
fc9c1026b1 | ||
|
|
aae49aeb63 | ||
|
|
f7a6f04792 | ||
|
|
e36ba9840c | ||
|
|
40bceb71f1 | ||
|
|
2d3bf91444 | ||
|
|
45ab258a6f | ||
|
|
600277660c | ||
|
|
6918afae52 | ||
|
|
07cfc4ad1c | ||
|
|
65eb9a503f | ||
|
|
f5d7cc1511 | ||
|
|
2cb0a48657 | ||
|
|
646f296039 | ||
|
|
a698c26d39 | ||
|
|
25136d6332 | ||
|
|
ee4b1ffef4 | ||
|
|
fe145836fe | ||
|
|
47100f4071 | ||
|
|
57311996c8 | ||
|
|
7ad8e48431 | ||
|
|
154d999b88 | ||
|
|
bde637c9c5 | ||
|
|
cb1c62f478 | ||
|
|
3727742419 | ||
|
|
f79bb38740 | ||
|
|
7d60ba85e7 | ||
|
|
cec7fa07c2 | ||
|
|
2be2380e3f | ||
|
|
ae99ade03a | ||
|
|
59731a93f1 | ||
|
|
ace55fbe7b | ||
|
|
696d3c5c81 | ||
|
|
a1e4ea3f5b | ||
|
|
6b054bd56c | ||
|
|
c8e2142dd9 | ||
|
|
1cf7ef6994 | ||
|
|
af8aa103bf | ||
|
|
d1f2c15a8f | ||
|
|
5a68c4d557 | ||
|
|
1438856531 | ||
|
|
694725b798 | ||
|
|
47cb4d0840 | ||
|
|
38ecb9ac06 | ||
|
|
6657a3d674 | ||
|
|
823ee21cea | ||
|
|
4d206af0ae | ||
|
|
2ff7b7efaa | ||
|
|
a5efc1c796 | ||
|
|
85d5ce77b1 | ||
|
|
6c4d4b79d6 | ||
|
|
364b1c5b31 | ||
|
|
4edf04ede5 | ||
|
|
01da1a70ea | ||
|
|
9bb7c7cd60 | ||
|
|
aaa7588dcd | ||
|
|
8d378d56ef | ||
|
|
476b1704b8 | ||
|
|
26c31ceeaf | ||
|
|
40f65ef5eb | ||
|
|
7736ccff86 | ||
|
|
437a547ec3 | ||
|
|
4ec65218da | ||
|
|
de45e9f808 | ||
|
|
87e768da0e | ||
|
|
daa773bff1 | ||
|
|
1e43835d0b | ||
|
|
efb8b3cd39 | ||
|
|
540899e92c | ||
|
|
ef6140d47f | ||
|
|
27ca5a8368 | ||
|
|
1a43242c24 | ||
|
|
65eeec6e17 | ||
|
|
d8e4dbd6cf | ||
|
|
32973e674f | ||
|
|
05fa6de119 | ||
|
|
cf0f4e0b96 | ||
|
|
0ae3c6c67b | ||
|
|
aae564d16c | ||
|
|
f6338ba844 | ||
|
|
d892645779 | ||
|
|
def40be5c0 | ||
|
|
d4bdf86fcb | ||
|
|
e6c3dc9d2b | ||
|
|
a3e98e5e51 | ||
|
|
769d1f2ddb | ||
|
|
f977ae4fe5 | ||
|
|
9b11c4c1e0 | ||
|
|
44afb98b3c | ||
|
|
1e246472cc | ||
|
|
77ee422c64 | ||
|
|
756d7e1f89 | ||
|
|
2ac4b7aad3 | ||
|
|
5726e2d3d2 | ||
|
|
7aea5754a9 | ||
|
|
c10adc37be | ||
|
|
465d81cbc2 | ||
|
|
0152e45190 | ||
|
|
029f7920d0 | ||
|
|
0f5184297c | ||
|
|
d15b3c86ad | ||
|
|
0a26cebaa5 | ||
|
|
16cf92cd17 | ||
|
|
2bac0458dd | ||
|
|
de9bb44bd0 | ||
|
|
0137fdf7e9 | ||
|
|
df14e42a0b | ||
|
|
c30dd0225b | ||
|
|
626fa422bc | ||
|
|
f08d9239fe | ||
|
|
1edd71df04 | ||
|
|
aff3bd4a64 | ||
|
|
ce1d623d60 |
No files matched your search
+1
-1
@@ -7,7 +7,7 @@ indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.java,*.kt}]
|
||||
[*.java]
|
||||
ij_java_wrap_long_lines = true
|
||||
|
||||
[{*.md,*.yml}]
|
||||
|
||||
@@ -9,9 +9,9 @@ name: Sonar CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "dev" ]
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main", "dev" ]
|
||||
branches: [ "main" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -22,12 +22,41 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build-sonar
|
||||
|
||||
- name: Upload Bukkit Artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
# Artifact name
|
||||
name: Sonar
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: bukkit/build/libs/Sonar-Bukkit.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload BungeeCord Artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
# Artifact name
|
||||
name: Sonar
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: bungeecord/build/libs/Sonar-Bungee.jar
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Velocity Artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
# Artifact name
|
||||
name: Sonar
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: velocity/build/libs/Sonar-Velocity.jar
|
||||
if-no-files-found: error
|
||||
@@ -3,11 +3,16 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
compileOnly(rootProject.libs.simpleyaml) {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
compileOnly(rootProject.libs.annotations)
|
||||
}
|
||||
|
||||
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||
tasks {
|
||||
shadowJar {
|
||||
archiveFileName = "sonar-api-${rootProject.version}.jar"
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
+14
-29
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -21,17 +21,17 @@ import net.kyori.adventure.audience.Audience;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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.database.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.notification.ActionBarNotificationHandler;
|
||||
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.UUID;
|
||||
@@ -41,19 +41,16 @@ public interface Sonar {
|
||||
|
||||
/**
|
||||
* Since we want to use Adventure on every server platform,
|
||||
* we have to use their platform module to support BungeeCord and Bukkit
|
||||
* we have to use their platform module to support BungeeCord and Bukkit.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@Nullable Audience audience(final @Nullable UUID uniqueId);
|
||||
|
||||
/**
|
||||
* @return The platform the plugin is being run on
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@NotNull Audience sender(final @NotNull Object object);
|
||||
|
||||
@NotNull SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the plugin logger so we can use the logger everywhere
|
||||
*/
|
||||
@NotNull LoggerWrapper getLogger();
|
||||
|
||||
@NotNull SonarConfiguration getConfig();
|
||||
@@ -62,29 +59,20 @@ public interface Sonar {
|
||||
|
||||
VerifiedPlayerController getVerifiedPlayerController();
|
||||
|
||||
@NotNull Verbose getVerboseHandler();
|
||||
@NotNull ActionBarNotificationHandler getActionBarNotificationHandler();
|
||||
|
||||
@NotNull Notification getNotificationHandler();
|
||||
@NotNull ChatNotificationHandler getChatNotificationHandler();
|
||||
|
||||
@NotNull SystemTimer getLaunchTimer();
|
||||
|
||||
@NotNull SonarStatistics getStatistics();
|
||||
|
||||
/**
|
||||
* Set a custom verbose handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setVerboseHandler(final @NotNull Verbose verboseHandler);
|
||||
void setActionBarNotificationHandler(final @NotNull ActionBarNotificationHandler notificationHandler);
|
||||
|
||||
/**
|
||||
* Set a custom notification handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setNotificationHandler(final @NotNull Notification notificationHandler);
|
||||
void setChatNotificationHandler(final @NotNull ChatNotificationHandler notificationHandler);
|
||||
|
||||
/**
|
||||
* Reloads the entire plugin
|
||||
*/
|
||||
void reload();
|
||||
|
||||
@NotNull
|
||||
@@ -94,24 +82,21 @@ public interface Sonar {
|
||||
|
||||
@NotNull
|
||||
default SonarEventManager getEventManager() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
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.
|
||||
return Fallback.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static Sonar get() {
|
||||
return SonarSupplier.get();
|
||||
return SonarSupplier.sonar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SonarPlatform {
|
||||
BUKKIT("Bukkit", 19110, "packet_handler",
|
||||
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder"),
|
||||
BUNGEE("BungeeCord", 19109, "inbound-boss",
|
||||
pipeline -> "packet-encoder"),
|
||||
VELOCITY("Velocity", 19107, "handler",
|
||||
pipeline -> "minecraft-encoder");
|
||||
|
||||
private final String displayName;
|
||||
private final int metricsId;
|
||||
private final String connectionHandler;
|
||||
private final Function<ChannelPipeline, String> encoder;
|
||||
}
|
||||
+5
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -20,20 +20,14 @@ package xyz.jonesdev.sonar.api;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@UtilityClass
|
||||
public class SonarSupplier {
|
||||
private Sonar sonar;
|
||||
Sonar sonar;
|
||||
|
||||
public void set(final @NotNull Sonar s) {
|
||||
public void set(final @NotNull Sonar _sonar) {
|
||||
if (sonar != null) {
|
||||
throw new AlreadyInitializedException();
|
||||
throw new IllegalStateException("Sonar is already initialized");
|
||||
}
|
||||
sonar = Objects.requireNonNull(s);
|
||||
}
|
||||
|
||||
public @NotNull Sonar get() {
|
||||
return sonar;
|
||||
sonar = _sonar;
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -42,7 +42,6 @@ public final class SonarVersion {
|
||||
this.formatted = version + " (" + gitCommit + ")";
|
||||
}
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/pull/1336/
|
||||
private static @NotNull Manifest getManifest() {
|
||||
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
|
||||
+2
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -21,7 +21,6 @@ import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
@@ -34,23 +33,12 @@ public final class InvocationSource {
|
||||
private final Predicate<String> permissionFunction;
|
||||
|
||||
/**
|
||||
* @return True, if {@link InvocationSource#uuid} is not null
|
||||
* @apiNote This indicates a player as a player will always have a UUID
|
||||
* @return true if {@link InvocationSource#uuid} is not null
|
||||
*/
|
||||
public boolean isPlayer() {
|
||||
return uuid != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*
|
||||
* @apiNote We should probably use cached components...
|
||||
* (See {@link InvocationSource#sendMessage(Component)})
|
||||
*/
|
||||
public void sendMessage(final String legacy) {
|
||||
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*/
|
||||
+32
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -26,20 +26,19 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.update.UpdateChecker;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public interface SonarCommand {
|
||||
List<String> TAB_SUGGESTIONS = new ArrayList<>();
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
Map<Integer, List<String>> TAB_SUGGESTIONS = new LinkedHashMap<>();
|
||||
|
||||
default void handle(final @NotNull InvocationSource source, final String @NotNull [] args) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (source.isPlayer() && !source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
if (Sonar.get().getConfig().getNoPermission() != null) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,11 +65,18 @@ public interface SonarCommand {
|
||||
// Invoke subcommand
|
||||
command.invoke(source, args);
|
||||
}, () -> {
|
||||
final UpdateChecker.CheckResult checkResult = UpdateChecker.getLastCheckResult();
|
||||
Component version = Component.text(Sonar.get().getVersion().getFormatted());
|
||||
if (checkResult.getConfigKey() != null) {
|
||||
version = version.append(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main." + checkResult.getConfigKey())));
|
||||
}
|
||||
|
||||
// Print standard help message
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>",
|
||||
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
|
||||
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
|
||||
Placeholder.component("version", version),
|
||||
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
|
||||
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
|
||||
@@ -94,30 +100,29 @@ public interface SonarCommand {
|
||||
});
|
||||
}
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// 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> arguments = Arrays.asList(subcommand.getInfo().arguments());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), arguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, arguments);
|
||||
static void prepareCachedTabSuggestions() {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
final List<String> subcommandsAndAliases = new ArrayList<>();
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
subcommandsAndAliases.add(subcommand.getInfo().name());
|
||||
|
||||
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
|
||||
TAB_SUGGESTIONS.put(subcommand.getInfo().name().hashCode(), arguments);
|
||||
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
subcommandsAndAliases.add(alias);
|
||||
TAB_SUGGESTIONS.put(alias.hashCode(), arguments);
|
||||
}
|
||||
}
|
||||
TAB_SUGGESTIONS.put(-1, subcommandsAndAliases);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
if (arguments.length <= 2) {
|
||||
final int subcommandHash = arguments.length <= 1 ? -1 : arguments[0].toLowerCase().hashCode();
|
||||
return TAB_SUGGESTIONS.getOrDefault(subcommandHash, Collections.emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -23,7 +23,6 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -91,7 +90,7 @@ public abstract class Subcommand {
|
||||
}
|
||||
|
||||
// Execute the sub command from the invocation source with the given arguments
|
||||
execute(new CommandInvocation(invocationSource, arguments));
|
||||
execute(invocationSource, arguments);
|
||||
}
|
||||
|
||||
public final @NotNull String getDescription() {
|
||||
@@ -99,5 +98,5 @@ public abstract class Subcommand {
|
||||
return Objects.requireNonNull(Sonar.get().getConfig().getMessagesConfig().getString(path));
|
||||
}
|
||||
|
||||
protected abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
protected abstract void execute(final @NotNull InvocationSource source, final String @NotNull [] args);
|
||||
}
|
||||
+1
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -25,34 +25,15 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface SubcommandInfo {
|
||||
|
||||
/**
|
||||
* @return Name of the subcommand
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* @return Aliases of the subcommand
|
||||
*/
|
||||
String[] aliases() default {};
|
||||
|
||||
/**
|
||||
* @return Arguments of the subcommand
|
||||
*/
|
||||
String[] arguments() default {};
|
||||
|
||||
/**
|
||||
* @return Whether the subcommands requires arguments
|
||||
*/
|
||||
boolean argumentsRequired() default true;
|
||||
|
||||
/**
|
||||
* @return Whether the subcommands can only be executed by a player
|
||||
*/
|
||||
boolean onlyPlayers() default false;
|
||||
|
||||
/**
|
||||
* @return Whether the subcommands can only be executed by console
|
||||
*/
|
||||
boolean onlyConsole() default false;
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
+1
-1
@@ -21,7 +21,7 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
enum Language {
|
||||
public enum Language {
|
||||
SYSTEM("system"),
|
||||
AB("Abkhazian"),
|
||||
AA("Afar"),
|
||||
+7
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -21,6 +21,7 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.simpleyaml.configuration.MemorySection;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
@@ -31,8 +32,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
|
||||
|
||||
@Getter
|
||||
public final class SimpleYamlConfig {
|
||||
private final File file;
|
||||
@@ -90,7 +89,7 @@ public final class SimpleYamlConfig {
|
||||
|
||||
public int getInt(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
|
||||
return 0;
|
||||
}
|
||||
return yaml.getInt(path);
|
||||
@@ -98,7 +97,7 @@ public final class SimpleYamlConfig {
|
||||
|
||||
public boolean getBoolean(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
|
||||
return false;
|
||||
}
|
||||
return yaml.getBoolean(path);
|
||||
@@ -106,7 +105,7 @@ public final class SimpleYamlConfig {
|
||||
|
||||
public @NotNull String getString(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
|
||||
return "";
|
||||
}
|
||||
final Object object = yaml.get(path);
|
||||
@@ -118,7 +117,7 @@ public final class SimpleYamlConfig {
|
||||
|
||||
public List<String> getStringList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getStringList(path);
|
||||
@@ -126,7 +125,7 @@ public final class SimpleYamlConfig {
|
||||
|
||||
public List<Integer> getIntList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getIntegerList(path);
|
||||
+109
-131
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import com.alessiodp.libby.Library;
|
||||
import com.j256.ormlite.db.DatabaseType;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
@@ -26,10 +27,9 @@ 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.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.database.ormlite.H2DatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.database.ormlite.MariaDbDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.database.ormlite.MysqlDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.io.File;
|
||||
@@ -48,24 +48,8 @@ public final class SonarConfiguration {
|
||||
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
|
||||
@Getter
|
||||
private final File languageFile, pluginFolder;
|
||||
|
||||
static final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[config] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[config] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[config] " + message, args);
|
||||
}
|
||||
};
|
||||
@Getter
|
||||
private Language language;
|
||||
|
||||
public SonarConfiguration(final @NotNull File pluginFolder) {
|
||||
this.pluginFolder = pluginFolder;
|
||||
@@ -82,14 +66,14 @@ public final class SonarConfiguration {
|
||||
final URL defaultLanguageFile = Sonar.class.getResource("/assets/language.properties");
|
||||
// Make sure the file actually exists before trying to copy it
|
||||
if (defaultLanguageFile == null) {
|
||||
LOGGER.error("Cannot check for custom language (is the file missing?)");
|
||||
Sonar.get().getLogger().error("Cannot check for custom language (is the file missing?)");
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
// Copy the file to the plugin data directory
|
||||
try (final InputStream inputStream = defaultLanguageFile.openStream()) {
|
||||
Files.copy(inputStream, languageFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException exception) {
|
||||
LOGGER.error("Error copying file: {}", exception);
|
||||
Sonar.get().getLogger().error("Error copying file: {}", exception);
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
}
|
||||
@@ -103,13 +87,13 @@ public final class SonarConfiguration {
|
||||
// Try parsing the property as a language
|
||||
return Language.fromCode(property);
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.error("Could not find requested language: {}", throwable);
|
||||
LOGGER.error("You can view a full list of valid language codes here:");
|
||||
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
|
||||
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
|
||||
Sonar.get().getLogger().error("Could not find requested language: {}", throwable);
|
||||
Sonar.get().getLogger().error("You can view a full list of valid language codes here:");
|
||||
Sonar.get().getLogger().error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
|
||||
Sonar.get().getLogger().error("If a translation does not exist yet, Sonar will use English (en).");
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
LOGGER.error("Error reading language file: {}", exception);
|
||||
Sonar.get().getLogger().error("Error reading language file: {}", exception);
|
||||
}
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
@@ -121,27 +105,27 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
// Generate the language file and check what it's set to
|
||||
Language preferredLanguage = getPreferredLanguage();
|
||||
if (preferredLanguage == Language.SYSTEM) {
|
||||
language = getPreferredLanguage();
|
||||
if (language == Language.SYSTEM) {
|
||||
try {
|
||||
// Try using the system language to determine the language file
|
||||
final String property = System.getProperty("user.language", "en");
|
||||
preferredLanguage = Language.fromCode(property);
|
||||
language = Language.fromCode(property);
|
||||
// Make sure the user knows that we're using the system language for translations
|
||||
LOGGER.info("Using system language ({}) for translations.", preferredLanguage);
|
||||
Sonar.get().getLogger().info("Using system language ({}) for translations.", language);
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Could not use system language for translations.");
|
||||
LOGGER.warn("Using default language ({}) for translations.", preferredLanguage);
|
||||
Sonar.get().getLogger().warn("Could not use system language for translations.");
|
||||
Sonar.get().getLogger().warn("Using default language ({}) for translations.", language);
|
||||
}
|
||||
} else {
|
||||
LOGGER.info("Using custom language ({}) for translations.", preferredLanguage);
|
||||
Sonar.get().getLogger().info("Using custom language ({}) for translations.", language);
|
||||
}
|
||||
|
||||
// Load all configurations
|
||||
try {
|
||||
generalConfig.load(getAsset("config", preferredLanguage));
|
||||
messagesConfig.load(getAsset("messages", preferredLanguage));
|
||||
webhookConfig.load(getAsset("webhook", preferredLanguage));
|
||||
generalConfig.load(getAsset("config", language));
|
||||
messagesConfig.load(getAsset("messages", language));
|
||||
webhookConfig.load(getAsset("webhook", language));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Error loading configuration", exception);
|
||||
}
|
||||
@@ -155,7 +139,7 @@ public final class SonarConfiguration {
|
||||
|
||||
// General settings
|
||||
logPlayerAddresses = generalConfig.getBoolean("general.log-player-addresses");
|
||||
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 1, 100);
|
||||
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 0, 99);
|
||||
|
||||
// Attack tracker
|
||||
minPlayersForAttack = clamp(generalConfig.getInt("attack-tracker.min-players-for-attack"), 2, 1024);
|
||||
@@ -165,12 +149,6 @@ public final class SonarConfiguration {
|
||||
|
||||
// Database
|
||||
database.type = Database.Type.valueOf(generalConfig.getString("database.type").toUpperCase());
|
||||
database.filename = generalConfig.getString("database.filename");
|
||||
database.host = generalConfig.getString("database.host");
|
||||
database.port = generalConfig.getInt("database.port");
|
||||
database.name = generalConfig.getString("database.name");
|
||||
database.username = generalConfig.getString("database.username");
|
||||
database.password = generalConfig.getString("database.password");
|
||||
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age"), 1, 365);
|
||||
|
||||
// Queue
|
||||
@@ -180,56 +158,47 @@ public final class SonarConfiguration {
|
||||
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
|
||||
// Warn the user if the verification timing is set to NEVER
|
||||
if (verification.timing == Verification.Timing.NEVER) {
|
||||
LOGGER.warn(" ");
|
||||
LOGGER.warn("You have set the verification timing to 'NEVER'.");
|
||||
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
|
||||
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
|
||||
LOGGER.warn("Please only edit this option if you really know what you are doing.");
|
||||
LOGGER.warn(" ");
|
||||
Sonar.get().getLogger().warn(" ");
|
||||
Sonar.get().getLogger().warn("You have set the verification timing to 'NEVER'.");
|
||||
Sonar.get().getLogger().warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
|
||||
Sonar.get().getLogger().warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
|
||||
Sonar.get().getLogger().warn("Please only edit this option if you really know what you are doing.");
|
||||
Sonar.get().getLogger().warn(" ");
|
||||
}
|
||||
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.collision.enabled");
|
||||
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
|
||||
|
||||
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
|
||||
verification.vehicle.enabled = generalConfig.getBoolean("verification.checks.vehicle.enabled");
|
||||
verification.vehicle.minimumPackets = clamp(generalConfig.getInt("verification.checks.vehicle.minimum-packets"), 0, 20);
|
||||
|
||||
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
|
||||
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
|
||||
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
|
||||
verification.map.bump = generalConfig.getBoolean("verification.checks.map-captcha.effects.bump");
|
||||
|
||||
verification.map.distortion.enabled = generalConfig.getBoolean("verification.checks.map-captcha.effects.distortion.enabled");
|
||||
verification.map.distortion.shape = generalConfig.getInt("verification.checks.map-captcha.effects.distortion.shape");
|
||||
verification.map.distortion.distance = generalConfig.getInt("verification.checks.map-captcha.effects.distortion.distance");
|
||||
verification.map.distortion.density = (float) generalConfig.getYaml().getDouble("verification.checks.map-captcha.effects.distortion.density");
|
||||
verification.map.distortion.mix = (float) generalConfig.getYaml().getDouble("verification.checks.map-captcha.effects.distortion.mix");
|
||||
|
||||
final String backgroundImagePath = generalConfig.getString("verification.checks.map-captcha.background");
|
||||
if (!backgroundImagePath.isEmpty()) {
|
||||
verification.map.backgroundImage = new File(pluginFolder, backgroundImagePath);
|
||||
if (!verification.map.backgroundImage.exists()) {
|
||||
Sonar.get().getLogger().error("The background image does not exist! Please check the configuration.");
|
||||
}
|
||||
} else {
|
||||
verification.map.backgroundImage = null;
|
||||
}
|
||||
|
||||
verification.map.autoColor = generalConfig.getBoolean("verification.checks.map-captcha.auto-color");
|
||||
verification.map.precomputeAmount = clamp(generalConfig.getInt("verification.checks.map-captcha.precompute"), 1, 1000);
|
||||
verification.map.precomputeAmount = clamp(generalConfig.getInt("verification.checks.map-captcha.precompute"), 10, 5000);
|
||||
verification.map.maxDuration = clamp(generalConfig.getInt("verification.checks.map-captcha.max-duration"), 5000, 360000);
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
|
||||
verification.map.alphabet = generalConfig.getString("verification.checks.map-captcha.alphabet");
|
||||
verification.map.backgroundImage = null;
|
||||
|
||||
final String backgroundPath = generalConfig.getString("verification.checks.map-captcha.background");
|
||||
if (!backgroundPath.isEmpty()) {
|
||||
final File backgroundFile = new File(pluginFolder, backgroundPath);
|
||||
if (backgroundFile.exists()) {
|
||||
verification.map.backgroundImage = backgroundFile;
|
||||
} else {
|
||||
Sonar.get().getLogger().error("Could not find background image {}", backgroundFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
|
||||
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
|
||||
verification.brand.maxLength = generalConfig.getInt("verification.checks.client-brand.max-length");
|
||||
|
||||
verification.timeOfDay = clamp(generalConfig.getInt("verification.time-of-day"), 0, 24000);
|
||||
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
|
||||
|
||||
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
|
||||
@@ -241,8 +210,6 @@ public final class SonarConfiguration {
|
||||
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
|
||||
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
|
||||
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
|
||||
verification.whitelistedProtocols.clear();
|
||||
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols"));
|
||||
verification.blacklistedProtocols.clear();
|
||||
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols"));
|
||||
|
||||
@@ -276,10 +243,16 @@ public final class SonarConfiguration {
|
||||
|
||||
// Messages
|
||||
prefix = MiniMessage.miniMessage().deserialize(messagesConfig.getString("prefix"));
|
||||
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
SonarCommand.prepareCachedMessages();
|
||||
final String noPermissionMessage = messagesConfig.getString("commands.no-permission");
|
||||
if (noPermissionMessage.isEmpty()) {
|
||||
noPermission = null;
|
||||
} else {
|
||||
noPermission = MiniMessage.miniMessage().deserialize(noPermissionMessage,
|
||||
Placeholder.component("prefix", prefix));
|
||||
}
|
||||
|
||||
SonarCommand.prepareCachedTabSuggestions();
|
||||
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
header = MiniMessage.miniMessage().deserialize(
|
||||
@@ -318,7 +291,7 @@ public final class SonarConfiguration {
|
||||
final String resourceName = url + "/" + language.getCode() + ".yml";
|
||||
URL result = Sonar.class.getResource("/assets/" + resourceName);
|
||||
if (result == null) {
|
||||
LOGGER.warn("Could not find " + resourceName + "! Using en.yml!");
|
||||
Sonar.get().getLogger().warn("Could not find " + resourceName + "! Using en.yml!");
|
||||
result = Objects.requireNonNull(Sonar.class.getResource("/assets/" + url + "/en.yml"));
|
||||
}
|
||||
return result;
|
||||
@@ -332,8 +305,8 @@ public final class SonarConfiguration {
|
||||
return output;
|
||||
}
|
||||
|
||||
public String formatAddress(final InetAddress inetAddress) {
|
||||
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
|
||||
public @NotNull String formatAddress(final @NotNull InetAddress inetAddress) {
|
||||
return logPlayerAddresses ? inetAddress.getHostAddress() : "<ip address withheld>";
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -397,26 +370,11 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private Timing timing;
|
||||
private boolean scratches;
|
||||
private boolean ripple;
|
||||
private boolean bump;
|
||||
private File backgroundImage;
|
||||
private boolean autoColor;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
private String dictionary;
|
||||
|
||||
private Smear distortion = new Smear();
|
||||
|
||||
@Getter
|
||||
public static final class Smear {
|
||||
private boolean enabled;
|
||||
private int shape;
|
||||
private int distance;
|
||||
private float density;
|
||||
private float mix;
|
||||
}
|
||||
private String alphabet;
|
||||
private File backgroundImage;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -424,25 +382,13 @@ public final class SonarConfiguration {
|
||||
private boolean enabled;
|
||||
private boolean checkCollisions;
|
||||
private boolean captchaOnFail;
|
||||
private Gamemode gamemode;
|
||||
private int maxMovementTicks;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2),
|
||||
// Keep this for backwards compatibility
|
||||
SPECTATOR(2);
|
||||
|
||||
private final int id;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Vehicle {
|
||||
private Timing timing;
|
||||
private boolean enabled;
|
||||
private int minimumPackets;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -452,6 +398,24 @@ public final class SonarConfiguration {
|
||||
private Pattern validRegex;
|
||||
}
|
||||
|
||||
private Gamemode gamemode;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
NOT_SET(-1),
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2);
|
||||
|
||||
private final int id;
|
||||
|
||||
public boolean isSurvivalOrAdventure() {
|
||||
return this == SURVIVAL || this == ADVENTURE;
|
||||
}
|
||||
}
|
||||
|
||||
private int timeOfDay;
|
||||
private boolean checkGeyser;
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
@@ -459,14 +423,12 @@ public final class SonarConfiguration {
|
||||
private Pattern validNameRegex;
|
||||
private Pattern validLocaleRegex;
|
||||
|
||||
private int maxLoginPackets;
|
||||
private int readTimeout;
|
||||
private int writeTimeout;
|
||||
private int reconnectDelay;
|
||||
private int rememberTime;
|
||||
private int blacklistTime;
|
||||
private int blacklistThreshold;
|
||||
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
|
||||
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
|
||||
|
||||
private Component tooFastReconnect;
|
||||
@@ -487,22 +449,38 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Type {
|
||||
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
|
||||
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
|
||||
H2("jdbc:h2:file:%s", new H2DatabaseTypeAdapter()),
|
||||
NONE(null, null);
|
||||
MYSQL("MySQL", "jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter(),
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("9.0.0")
|
||||
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
|
||||
.build()),
|
||||
MARIADB("MariaDB", "jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter(),
|
||||
Library.builder()
|
||||
.groupId("org{}mariadb{}jdbc")
|
||||
.artifactId("mariadb-java-client")
|
||||
.version("3.4.1")
|
||||
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
|
||||
.build()),
|
||||
H2("H2", "jdbc:h2:file:%s", new H2DatabaseTypeAdapter(),
|
||||
Library.builder()
|
||||
.groupId("com{}h2database")
|
||||
.artifactId("h2")
|
||||
.version("2.2.220")
|
||||
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
|
||||
.build()),
|
||||
NONE("None", null, null, null);
|
||||
|
||||
private final String displayName;
|
||||
private final String connectionString;
|
||||
private final DatabaseType databaseType;
|
||||
private final Library databaseDriver;
|
||||
@Setter
|
||||
private boolean loaded;
|
||||
}
|
||||
|
||||
private Type type;
|
||||
private String filename;
|
||||
private String host;
|
||||
private int port;
|
||||
private String name;
|
||||
private String username;
|
||||
private String password;
|
||||
private int maximumAge;
|
||||
}
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.database.controller;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.j256.ormlite.dao.Dao;
|
||||
import com.j256.ormlite.dao.DaoManager;
|
||||
import com.j256.ormlite.jdbc.JdbcConnectionSource;
|
||||
import com.j256.ormlite.stmt.QueryBuilder;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import com.j256.ormlite.table.TableUtils;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class VerifiedPlayerController {
|
||||
@Getter
|
||||
private final Set<String> cache = Collections.synchronizedSet(new HashSet<>());
|
||||
private @Nullable ConnectionSource connectionSource;
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@Getter
|
||||
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
|
||||
private final ExecutorService updateService = Executors.newSingleThreadExecutor();
|
||||
|
||||
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
cachedDatabaseType = database.getType();
|
||||
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
Sonar.get().getLogger().warn("Configure a database to save verified players.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure to only load the driver once per database type
|
||||
if (!cachedDatabaseType.isLoaded()) {
|
||||
Sonar.get().getLogger().info("Loading {} driver version {}",
|
||||
cachedDatabaseType.getDatabaseType().getDatabaseName(),
|
||||
cachedDatabaseType.getDatabaseDriver().getVersion());
|
||||
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
|
||||
cachedDatabaseType.setLoaded(true);
|
||||
}
|
||||
|
||||
try {
|
||||
final String jdbcURL;
|
||||
|
||||
// H2 has a different JDBC URL layout
|
||||
// https://www.codejava.net/java-se/jdbc/connect-to-h2-database-examples
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.H2) {
|
||||
final File file = new File(Sonar.get().getConfig().getPluginFolder(),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.filename"));
|
||||
jdbcURL = String.format(cachedDatabaseType.getConnectionString(), file.getAbsolutePath());
|
||||
} else {
|
||||
// Normal JDBC URL layout for MySQL/MariaDB/...
|
||||
jdbcURL = String.format(cachedDatabaseType.getConnectionString(),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.host"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getInt("database.port"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
|
||||
}
|
||||
|
||||
connectionSource = new JdbcConnectionSource(jdbcURL,
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
|
||||
cachedDatabaseType.getDatabaseType());
|
||||
|
||||
try {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException ignored) {
|
||||
/*
|
||||
* This is caused by a 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);
|
||||
queryBuilder = dao.queryBuilder();
|
||||
|
||||
// Make sure to run the clean task and the caching task in the same thread
|
||||
// https://github.com/jonesdevelopment/sonar/issues/150
|
||||
updateService.execute(() -> {
|
||||
if (connectionSource != null) {
|
||||
try {
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(database.getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(verifiedPlayer -> cache.add(verifiedPlayer.getFingerprint()));
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection if instantiated
|
||||
*/
|
||||
public void close() {
|
||||
// The connection source will always be null if the database type is NONE.
|
||||
if (connectionSource != null) {
|
||||
try {
|
||||
connectionSource.close();
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
// Make sure not to reuse a closed connection
|
||||
connectionSource = null;
|
||||
updateService.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp
|
||||
*/
|
||||
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
|
||||
final long timestamp = Instant.now().minus(maximumAge, ChronoUnit.DAYS).getEpochSecond() * 1000L;
|
||||
|
||||
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
|
||||
.lt("timestamp", new Timestamp(timestamp))
|
||||
.query();
|
||||
|
||||
if (oldEntries != null && !oldEntries.isEmpty()) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First, remove the player from the local cache and then,
|
||||
* secondly, asynchronously add the player to the database.
|
||||
*
|
||||
* @param fingerprint Fingerprint of the verified player
|
||||
*/
|
||||
public void remove(final @NotNull String fingerprint) {
|
||||
cache.remove(fingerprint);
|
||||
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateService.execute(() -> {
|
||||
if (connectionSource != null) {
|
||||
try {
|
||||
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
|
||||
.eq("fingerprint", fingerprint)
|
||||
.query();
|
||||
|
||||
if (verifiedPlayer != null) {
|
||||
for (final VerifiedPlayer player : verifiedPlayer) {
|
||||
dao.delete(player);
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new VerifiedPlayer model from the given username, host address, and timestamp
|
||||
*/
|
||||
public void add(final @NotNull String username, final @NotNull String hostAddress, final long timestamp) {
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
// Add a new VerifiedPlayer object from the given parameters to the database
|
||||
add(new VerifiedPlayer(fingerprint, timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the player locally and then adds the player to the database
|
||||
*/
|
||||
public void add(final @NotNull VerifiedPlayer player) {
|
||||
cache.add(player.getFingerprint());
|
||||
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateService.execute(() -> {
|
||||
if (connectionSource != null) {
|
||||
try {
|
||||
dao.createIfNotExists(player);
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the local cache, and, if the database type is set,
|
||||
* remove the table from the database.
|
||||
*/
|
||||
public void clearAll() {
|
||||
cache.clear();
|
||||
|
||||
// Delete the entire table from the database, if necessary
|
||||
if (connectionSource != null
|
||||
&& cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
try {
|
||||
dao.deleteBuilder().delete();
|
||||
} catch (SQLException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.model;
|
||||
package xyz.jonesdev.sonar.api.database.model;
|
||||
|
||||
import com.j256.ormlite.field.DatabaseField;
|
||||
import com.j256.ormlite.table.DatabaseTable;
|
||||
@@ -25,34 +25,23 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@DatabaseTable(tableName = "sonar_fingerprints")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@DatabaseTable(tableName = "sonar_verified_players")
|
||||
public final class VerifiedPlayer {
|
||||
@SuppressWarnings("unused")
|
||||
@DatabaseField(generatedId = true)
|
||||
private int id;
|
||||
|
||||
@DatabaseField(
|
||||
columnName = "ip_address",
|
||||
columnName = "fingerprint",
|
||||
canBeNull = false,
|
||||
uniqueIndexName = "ip_address_player_uuid_idx",
|
||||
width = 16
|
||||
width = 48
|
||||
)
|
||||
private String inetAddress;
|
||||
|
||||
@DatabaseField(
|
||||
columnName = "player_uuid",
|
||||
canBeNull = false,
|
||||
uniqueIndexName = "ip_address_player_uuid_idx",
|
||||
width = 36
|
||||
)
|
||||
private UUID playerUuid;
|
||||
private String fingerprint;
|
||||
|
||||
@DatabaseField(
|
||||
columnName = "timestamp",
|
||||
@@ -60,17 +49,9 @@ public final class VerifiedPlayer {
|
||||
)
|
||||
private Timestamp timestamp;
|
||||
|
||||
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
public VerifiedPlayer(final @NotNull String fingerprint,
|
||||
final long timestamp) {
|
||||
this(inetAddress.toString(), playerUuid, timestamp);
|
||||
}
|
||||
|
||||
public VerifiedPlayer(final @NotNull String inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this.inetAddress = inetAddress;
|
||||
this.playerUuid = playerUuid;
|
||||
this.fingerprint = fingerprint;
|
||||
this.timestamp = new Timestamp(timestamp);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
package xyz.jonesdev.sonar.api.database.ormlite;
|
||||
|
||||
import com.j256.ormlite.jdbc.db.H2DatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
package xyz.jonesdev.sonar.api.database.ormlite;
|
||||
|
||||
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
package xyz.jonesdev.sonar.api.database.ormlite;
|
||||
|
||||
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,13 +17,13 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SonarEventListener {
|
||||
|
||||
/**
|
||||
* Handles the publishing of an event
|
||||
*
|
||||
* @param event [Cancellable] Sonar event
|
||||
* Handles the publishing of an event through Sonar's API
|
||||
*/
|
||||
void handle(final SonarEvent event);
|
||||
void handle(final @NotNull SonarEvent event);
|
||||
}
|
||||
+12
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -35,12 +35,9 @@ public final class 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) {
|
||||
// Don't post a task if there are no listeners
|
||||
// Don't post an event if there are no listeners
|
||||
if (EVENT_LISTENERS.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -61,11 +58,13 @@ public final class SonarEventManager {
|
||||
* Registers one (or more) event listeners
|
||||
*
|
||||
* @param listeners One (or more) listeners to register
|
||||
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
|
||||
* @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));
|
||||
public void registerListener(final @NotNull SonarEventListener... listeners) {
|
||||
synchronized (EVENT_LISTENERS) {
|
||||
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,10 +72,12 @@ public final class SonarEventManager {
|
||||
*
|
||||
* @param listeners One (or more) listeners to unregister
|
||||
* @apiNote This does not have any effect if the given listeners are not registered
|
||||
* @see #registerListener(SonarEventListener...) Register an event listener
|
||||
* @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));
|
||||
public void unregisterListener(final @NotNull SonarEventListener... listeners) {
|
||||
synchronized (EVENT_LISTENERS) {
|
||||
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,16 +17,14 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class AttackMitigatedEvent implements SonarEvent {
|
||||
private final AttackTracker.AttackStatistics attackStatistics;
|
||||
+8
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,16 +15,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = "rawArguments")
|
||||
public final class CommandInvocation {
|
||||
private final InvocationSource source;
|
||||
private final String[] rawArguments;
|
||||
public final class CaptchaGenerationEndEvent implements SonarEvent {
|
||||
private final SystemTimer timer;
|
||||
private final int amountGenerated;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public final class CaptchaGenerationStartEvent implements SonarEvent {
|
||||
private CaptchaGenerator captchaGenerator;
|
||||
}
|
||||
+1
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,19 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserBlacklistedEvent implements SonarEvent {
|
||||
@ApiStatus.Experimental
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+1
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final @NotNull FallbackUser user;
|
||||
+1
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -26,9 +25,7 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyJoinEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+1
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,22 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final UUID offlineUuid;
|
||||
private final FallbackUser user;
|
||||
private final long timeTakenToVerify;
|
||||
}
|
||||
+10
-33
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -22,10 +22,10 @@ import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
|
||||
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -36,36 +36,17 @@ import java.util.concurrent.ConcurrentMap;
|
||||
public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
|
||||
// Map of all players connected to the server in general
|
||||
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
|
||||
// Map of all connected IP addresses (used for fast checking)
|
||||
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
private final ConcurrentMap<InetAddress, Boolean> connected = new ConcurrentHashMap<>(256);
|
||||
private final FallbackLoginQueue queue = new FallbackLoginQueue();
|
||||
@Setter
|
||||
private Cache<InetAddress, Byte> blacklist;
|
||||
private Cache<String, Integer> blacklist;
|
||||
@Setter
|
||||
private long blacklistTime;
|
||||
|
||||
private final @NotNull FallbackQueue queue = new FallbackQueue();
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
|
||||
|
||||
private final LoggerWrapper logger = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[fallback] " + message, args);
|
||||
}
|
||||
};
|
||||
@Setter
|
||||
private CaptchaGenerator captchaGenerator;
|
||||
@Setter
|
||||
private Ratelimiter<InetAddress> ratelimiter;
|
||||
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
|
||||
@@ -75,10 +56,6 @@ public final class Fallback {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
}
|
||||
|
||||
public boolean shouldPerformVehicleCheck() {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getVehicle().getTiming());
|
||||
}
|
||||
|
||||
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
+13
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -23,8 +23,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -32,27 +30,24 @@ import java.util.concurrent.Executors;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackQueue {
|
||||
public final class FallbackLoginQueue {
|
||||
// Fixed thread pool executor for all new verifications
|
||||
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
|
||||
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
|
||||
|
||||
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
|
||||
128, 0.75f, Runtime.getRuntime().availableProcessors());
|
||||
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(512);
|
||||
|
||||
public void poll() {
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
// Iterate through the map and process up to maxQueuePolls entries
|
||||
final int maxQueuePolls = Math.min(players.size(), Sonar.get().getConfig().getQueue().getMaxQueuePolls());
|
||||
// No need to initiate an executor service task if nobody is currently queued
|
||||
if (maxQueuePolls <= 0) return;
|
||||
// We need to be cautious here since we don't want any concurrency issues
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = players.entrySet().iterator();
|
||||
while (iterator.hasNext() && index++ < maxQueuePolls) {
|
||||
// Run the cached runnable
|
||||
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
|
||||
QUEUE_EXECUTOR.execute(entry.getValue());
|
||||
// Remove runnable from the map
|
||||
iterator.remove();
|
||||
}
|
||||
QUEUE_EXECUTOR.execute(() -> {
|
||||
final var iterator = players.entrySet().iterator();
|
||||
for (int index = 0; index < maxQueuePolls && iterator.hasNext(); index++) {
|
||||
iterator.next().getValue().run();
|
||||
iterator.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+11
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -18,8 +18,14 @@
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
public interface FallbackPipelines {
|
||||
String FALLBACK_HANDLER = "sonar-fallback-handler";
|
||||
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
|
||||
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
|
||||
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
|
||||
String FALLBACK_INACTIVE_LISTENER = "sonar-inactive-listener";
|
||||
String FALLBACK_INBOUND_HANDLER = "sonar-inbound-handler";
|
||||
String FALLBACK_FRAME_DECODER = "sonar-frame-decoder";
|
||||
String FALLBACK_FRAME_ENCODER = "sonar-frame-encoder";
|
||||
String FALLBACK_TIMEOUT = "sonar-timeout";
|
||||
String FALLBACK_PACKET_HANDLER = "sonar-packet-handler";
|
||||
String FALLBACK_PACKET_ENCODER = "sonar-packet-encoder";
|
||||
String FALLBACK_PACKET_DECODER = "sonar-packet-decoder";
|
||||
String FALLBACK_TAIL_EXCEPTIONS = "sonar-exception-tail";
|
||||
String FALLBACK_BANDWIDTH = "sonar-bandwidth-counter";
|
||||
}
|
||||
+12
-41
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -26,14 +25,10 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser {
|
||||
@NotNull
|
||||
Channel getChannel();
|
||||
|
||||
@NotNull
|
||||
ChannelPipeline getPipeline();
|
||||
Channel channel();
|
||||
|
||||
@NotNull
|
||||
InetAddress getInetAddress();
|
||||
@@ -44,13 +39,15 @@ public interface FallbackUser {
|
||||
@NotNull
|
||||
SystemTimer getLoginTimer();
|
||||
|
||||
boolean isReceivedClientSettings();
|
||||
@NotNull
|
||||
String getFingerprint();
|
||||
|
||||
void setReceivedClientSettings(final boolean receivedClientSettings);
|
||||
@NotNull
|
||||
String getUsername();
|
||||
|
||||
boolean isReceivedPluginMessage();
|
||||
boolean isForceCaptcha();
|
||||
|
||||
void setReceivedPluginMessage(final boolean receivedPluginMessage);
|
||||
void setForceCaptcha(final boolean isForceCaptcha);
|
||||
|
||||
boolean isGeyser();
|
||||
|
||||
@@ -62,28 +59,14 @@ public interface FallbackUser {
|
||||
*/
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
*
|
||||
* @param username Username of the player
|
||||
* @param uuid UUID of the player
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
* @param handler Name of the main pipeline
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String handler);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
*
|
||||
* @param msg Message to send to the player
|
||||
*/
|
||||
default void write(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().writeAndFlush(msg, getChannel().voidPromise());
|
||||
if (channel().isActive()) {
|
||||
channel().writeAndFlush(msg, channel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
@@ -94,22 +77,10 @@ public interface FallbackUser {
|
||||
* sent once all messages are flushed.
|
||||
*/
|
||||
default void delayedWrite(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().write(msg, getChannel().voidPromise());
|
||||
if (channel().isActive()) {
|
||||
channel().write(msg, channel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the player who failed the verification
|
||||
* and caches them in FAILED_VERIFICATIONS.
|
||||
* If the player fails the verification twice,
|
||||
* the player will be temporarily denied from verifying.
|
||||
*
|
||||
* @param reason Reason for failing the verification
|
||||
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
|
||||
* will not be thrown if no reason is given
|
||||
*/
|
||||
void fail(final @NotNull String reason);
|
||||
}
|
||||
+8
-12
@@ -15,21 +15,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
package xyz.jonesdev.sonar.api.fallback.captcha;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SonarPlatform {
|
||||
BUKKIT("Bukkit", 19110),
|
||||
BUNGEE("BungeeCord", 19109),
|
||||
VELOCITY("Velocity", 19107);
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CaptchaGenerator {
|
||||
|
||||
private final String displayName;
|
||||
/**
|
||||
* bStats service ID for the respective Sonar platform
|
||||
* Generates a {@link java.awt.image.BufferedImage} that shows the answer to the CAPTCHA
|
||||
*/
|
||||
private final int metricsId;
|
||||
@NotNull BufferedImage createImage(final char @NotNull [] answer);
|
||||
}
|
||||
+19
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -68,7 +68,8 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765),
|
||||
MINECRAFT_1_20_5(766),
|
||||
MINECRAFT_1_21(767);
|
||||
MINECRAFT_1_21(767),
|
||||
MINECRAFT_1_21_2(768);
|
||||
|
||||
private final int protocol;
|
||||
|
||||
@@ -90,6 +91,22 @@ public enum ProtocolVersion {
|
||||
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
|
||||
}
|
||||
|
||||
public boolean greaterThan(final ProtocolVersion that) {
|
||||
return compareTo(that) > 0;
|
||||
}
|
||||
|
||||
public boolean greaterThanOrEquals(final ProtocolVersion that) {
|
||||
return compareTo(that) >= 0;
|
||||
}
|
||||
|
||||
public boolean lessThan(final ProtocolVersion that) {
|
||||
return compareTo(that) < 0;
|
||||
}
|
||||
|
||||
public boolean lessThanOrEquals(final ProtocolVersion that) {
|
||||
return compareTo(that) <= 0;
|
||||
}
|
||||
|
||||
public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
|
||||
return compareTo(first) >= 0 && compareTo(last) <= 0;
|
||||
}
|
||||
+6
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,12 +15,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.command.subcommand.argument;
|
||||
package xyz.jonesdev.sonar.api.fallback.ratelimit;
|
||||
|
||||
public @interface Argument {
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* @return Returns the name of the argument
|
||||
*/
|
||||
String value();
|
||||
@FunctionalInterface
|
||||
public interface Ratelimiter<T> {
|
||||
boolean attempt(final @NotNull T t);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.fingerprint;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@UtilityClass
|
||||
public class FingerprintingUtil {
|
||||
|
||||
/**
|
||||
* Returns a hex string representing a hash of the username and IP address
|
||||
*/
|
||||
public @NotNull String getFingerprint(final @NotNull String username,
|
||||
final @NotNull String hostAddress) {
|
||||
final int hash0 = username.hashCode();
|
||||
final int hash1 = hostAddress.hashCode();
|
||||
final int combined = hash0 + hash1;
|
||||
return Integer.toHexString(hash0 >> 4)
|
||||
+ Integer.toHexString(combined << 2)
|
||||
+ Integer.toHexString(hash1 >> 4);
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -33,7 +33,6 @@ public interface LoggerWrapper {
|
||||
|
||||
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();
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
package xyz.jonesdev.sonar.api.notification;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
@@ -23,21 +23,21 @@ import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
|
||||
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
|
||||
|
||||
@Getter
|
||||
public final class Verbose extends Observable {
|
||||
public final class ActionBarNotificationHandler extends NotificationHandler {
|
||||
private int animationIndex;
|
||||
|
||||
@Override
|
||||
public void observe() {
|
||||
public void handleNotification() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
@@ -63,10 +63,10 @@ public final class Verbose extends Observable {
|
||||
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
|
||||
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getPerSecondIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getPerSecondOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalIncomingBandwidth())),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalOutgoingBandwidth())),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
|
||||
+9
-9
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
package xyz.jonesdev.sonar.api.notification;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
@@ -29,13 +29,13 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
|
||||
|
||||
@Getter
|
||||
public final class Notification extends Observable {
|
||||
public final class ChatNotificationHandler extends NotificationHandler {
|
||||
|
||||
@Override
|
||||
public void observe() {
|
||||
public void handleNotification() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
@@ -63,10 +63,10 @@ public final class Notification extends Observable {
|
||||
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
|
||||
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getPerSecondIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getPerSecondOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalIncomingBandwidth())),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalOutgoingBandwidth())),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
|
||||
@@ -74,7 +74,7 @@ public final class Notification extends Observable {
|
||||
}
|
||||
|
||||
// Send the title and chat messages to all online players
|
||||
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
|
||||
for (final UUID subscriber : Sonar.get().getChatNotificationHandler().getSubscribers()) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
package xyz.jonesdev.sonar.api.notification;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -25,10 +25,10 @@ import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
@Getter
|
||||
public abstract class Observable {
|
||||
public abstract class NotificationHandler {
|
||||
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
|
||||
public abstract void observe();
|
||||
public abstract void handleNotification();
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.jvm;
|
||||
package xyz.jonesdev.sonar.api.profiler;
|
||||
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
import lombok.experimental.UtilityClass;
|
||||
@@ -24,7 +24,7 @@ import java.lang.management.ManagementFactory;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@UtilityClass
|
||||
public class JVMProcessInformation {
|
||||
public class SimpleProcessProfiler {
|
||||
private final Runtime RUNTIME = Runtime.getRuntime();
|
||||
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.statistics;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface SonarStatistics {
|
||||
long getConnectionsPerSecond();
|
||||
|
||||
long getLoginsPerSecond();
|
||||
|
||||
long getCurrentIncomingBandwidth();
|
||||
|
||||
long getCurrentOutgoingBandwidth();
|
||||
|
||||
long getTotalIncomingBandwidth();
|
||||
|
||||
long getTotalOutgoingBandwidth();
|
||||
|
||||
String getPerSecondIncomingBandwidthFormatted();
|
||||
|
||||
String getPerSecondOutgoingBandwidthFormatted();
|
||||
|
||||
int getTotalPlayersJoined();
|
||||
|
||||
int getTotalPlayersVerified();
|
||||
|
||||
int getTotalSuccessfulVerifications();
|
||||
|
||||
int getTotalFailedVerifications();
|
||||
|
||||
long getCurrentAttemptedVerifications();
|
||||
|
||||
int getTotalAttemptedVerifications();
|
||||
|
||||
long getCurrentBlacklistSize();
|
||||
|
||||
long getTotalBlacklistSize();
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
+7
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.attack;
|
||||
package xyz.jonesdev.sonar.api.tracker;
|
||||
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -27,12 +27,13 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AttackTracker {
|
||||
public static final AttackTracker INSTANCE = new AttackTracker();
|
||||
|
||||
private @Nullable AttackStatistics currentAttack;
|
||||
private int attackThreshold;
|
||||
|
||||
@@ -65,10 +66,10 @@ public final class AttackTracker {
|
||||
// An attack has been detected
|
||||
if (currentAttack == null) {
|
||||
currentAttack = new AttackStatistics();
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().getCache().size();
|
||||
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
Sonar.get().getNotificationHandler().observe();
|
||||
Sonar.get().getChatNotificationHandler().handleNotification();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -117,7 +118,7 @@ public final class AttackTracker {
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().getCache().size();
|
||||
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
|
||||
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
@@ -143,11 +144,9 @@ public final class AttackTracker {
|
||||
|
||||
// Reset the attack status
|
||||
currentAttack = null;
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
} else {
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
+30
-29
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,14 +15,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.update;
|
||||
package xyz.jonesdev.sonar.api.update;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.BufferedReader;
|
||||
@@ -36,24 +37,8 @@ import java.util.concurrent.Executors;
|
||||
@UtilityClass
|
||||
public class UpdateChecker {
|
||||
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[update-checker] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[update-checker] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[update-checker] " + message, args);
|
||||
}
|
||||
};
|
||||
@Getter
|
||||
private CheckResult lastCheckResult = CheckResult.UNRESOLVED;
|
||||
|
||||
public void checkForUpdates() {
|
||||
ASYNC_EXECUTOR.execute(() -> {
|
||||
@@ -66,17 +51,21 @@ public class UpdateChecker {
|
||||
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getVersion());
|
||||
|
||||
if (convertedCurrentVersion < convertedLatestVersion) {
|
||||
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
|
||||
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
|
||||
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
|
||||
Sonar.get().getLogger().warn("A new version of Sonar is available: {}", latestStableRelease);
|
||||
Sonar.get().getLogger().warn("Please make sure to update to the latest version to ensure stability and security:");
|
||||
Sonar.get().getLogger().warn("https://github.com/jonesdevelopment/sonar/releases/latest");
|
||||
lastCheckResult = CheckResult.OUTDATED_VERSION;
|
||||
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().getGitBranch().equals("main")) {
|
||||
LOGGER.warn("You are currently using an unreleased version of Sonar!");
|
||||
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
|
||||
} else {
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
Sonar.get().getLogger().warn("You are currently using an unreleased version of Sonar!");
|
||||
Sonar.get().getLogger().warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
|
||||
lastCheckResult = CheckResult.UNSTABLE_VERSION;
|
||||
} else if (lastCheckResult != CheckResult.LATEST_VERSION) {
|
||||
Sonar.get().getLogger().info("You are currently using the latest stable release of Sonar!");
|
||||
lastCheckResult = CheckResult.LATEST_VERSION;
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.warn("Unable to retrieve version information: {}", throwable);
|
||||
Sonar.get().getLogger().warn("Unable to retrieve version information: {}", throwable);
|
||||
lastCheckResult = CheckResult.API_ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -118,4 +107,16 @@ public class UpdateChecker {
|
||||
final int patch = Integer.parseInt(convertedParts[2]);
|
||||
return convertedMajor + convertedMinor + patch; // sum the converted version parts
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum CheckResult {
|
||||
API_ERROR("api-error"),
|
||||
OUTDATED_VERSION("outdated-version"),
|
||||
UNSTABLE_VERSION("unstable-version"),
|
||||
LATEST_VERSION(null),
|
||||
UNRESOLVED(null);
|
||||
|
||||
private final String configKey;
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -56,8 +56,7 @@ public final class DiscordWebhook {
|
||||
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// Warn the user if the connection might've not been successful
|
||||
// I don't know why Discord doesn't respond with OK, but I guess this should be fine
|
||||
// Discord replies with HTTP_NO_CONTENT (204) if the request is successful
|
||||
final int code = urlConnection.getResponseCode();
|
||||
if (code != HTTP_NO_CONTENT) {
|
||||
Sonar.get().getLogger().warn("Unexpected Discord webhook response code {}", code);
|
||||
+3
-3
@@ -14,7 +14,7 @@ allprojects {
|
||||
}
|
||||
|
||||
apply(plugin = "java")
|
||||
apply(plugin = "io.github.goooler.shadow")
|
||||
apply(plugin = "com.gradleup.shadow")
|
||||
|
||||
dependencies {
|
||||
compileOnly(rootProject.libs.lombok)
|
||||
@@ -48,8 +48,8 @@ allprojects {
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
|
||||
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
|
||||
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
|
||||
relocate("org.h2", "xyz.jonesdev.sonar.libs.h2")
|
||||
relocate("com.jhlabs", "xyz.jonesdev.sonar.libs.jhlabs")
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
@@ -86,7 +86,7 @@ allprojects {
|
||||
tasks {
|
||||
// This is a small wrapper tasks to simplify the building process
|
||||
register("build-sonar") {
|
||||
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
|
||||
val subprojects = listOf("api", "captcha", "common", "bukkit", "bungeecord", "velocity")
|
||||
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
|
||||
dependsOn(buildTasks)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
|
||||
|
||||
plugins {
|
||||
id("net.minecrell.plugin-yml.bukkit") version "0.6.0" apply true
|
||||
alias(libs.plugins.pluginyml.bukkit) apply true
|
||||
}
|
||||
|
||||
bukkit {
|
||||
name = rootProject.name
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
|
||||
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
|
||||
apiVersion = "1.13" // ignore legacy plugin warning
|
||||
foliaSupported = true
|
||||
|
||||
commands {
|
||||
@@ -33,11 +32,11 @@ dependencies {
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly(rootProject.libs.spigot)
|
||||
testCompileOnly(rootProject.libs.spigot)
|
||||
|
||||
implementation(rootProject.libs.adventure.platform.bukkit)
|
||||
implementation(rootProject.libs.adventure.platform.api)
|
||||
implementation(rootProject.libs.adventure.platform.facet)
|
||||
implementation(rootProject.libs.adventure.nbt)
|
||||
implementation(rootProject.libs.adventure.platform.bukkit) {
|
||||
exclude(module = "adventure-nbt")
|
||||
}
|
||||
implementation(rootProject.libs.bstats.bukkit)
|
||||
implementation(rootProject.libs.libby.bukkit)
|
||||
}
|
||||
+28
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -23,24 +23,24 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
|
||||
import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Getter
|
||||
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(),
|
||||
new BukkitLibraryManager(plugin, plugin.getDataFolder().getName()));
|
||||
INSTANCE = this;
|
||||
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +56,11 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
return bukkitAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Audience sender(final @NotNull Object object) {
|
||||
return bukkitAudiences.sender((CommandSender) object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -82,6 +87,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
private Metrics metrics;
|
||||
|
||||
public static final CompletableFuture<Void> INITIALIZE_LISTENER = new CompletableFuture<>();
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
// Initialize bStats.org metrics
|
||||
@@ -92,15 +99,29 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
() -> getConfig().getVerification().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("captcha",
|
||||
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("language",
|
||||
() -> getConfig().getLanguage().getName()));
|
||||
metrics.addCustomChart(new SimplePie("database_type",
|
||||
() -> getConfig().getDatabase().getType().getDisplayName()));
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Try to inject into the server
|
||||
if (FallbackBukkitInjector.isLateBindEnabled()) {
|
||||
getPlugin().getServer().getScheduler().runTask(getPlugin(), FallbackBukkitInjector::inject);
|
||||
} else {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new BukkitJoinListener(), getPlugin());
|
||||
}
|
||||
|
||||
// Let the injector know that the plugin has been enabled
|
||||
INITIALIZE_LISTENER.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Make sure to properly stop the metrics
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -18,10 +18,19 @@
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
|
||||
|
||||
public final class SonarBukkitPlugin extends JavaPlugin {
|
||||
private SonarBukkit bootstrap;
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
// Inject early if late-bind is disabled
|
||||
if (!FallbackBukkitInjector.isLateBindEnabled()) {
|
||||
FallbackBukkitInjector.inject();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
bootstrap = new SonarBukkit(this);
|
||||
+6
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -23,14 +23,13 @@ import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
|
||||
|
||||
@Override
|
||||
@@ -41,11 +40,11 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
Sonar.get().sender(sender),
|
||||
sender::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
return true; // Valid
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,9 +53,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
final String commandAlias,
|
||||
final String @NotNull [] args) {
|
||||
// Do not allow tab completion if the player does not have the required permission
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
return getCachedTabSuggestions(args);
|
||||
return sender.hasPermission("sonar.command") ? getCachedTabSuggestions(args) : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.fallback;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum BukkitServerVersion {
|
||||
MINECRAFT_1_7_10,
|
||||
MINECRAFT_1_8,
|
||||
MINECRAFT_1_8_3,
|
||||
MINECRAFT_1_8_8,
|
||||
MINECRAFT_1_9,
|
||||
MINECRAFT_1_9_2,
|
||||
MINECRAFT_1_9_4,
|
||||
MINECRAFT_1_10,
|
||||
MINECRAFT_1_10_1,
|
||||
MINECRAFT_1_10_2,
|
||||
MINECRAFT_1_11,
|
||||
MINECRAFT_1_11_2,
|
||||
MINECRAFT_1_12,
|
||||
MINECRAFT_1_12_1,
|
||||
MINECRAFT_1_12_2,
|
||||
MINECRAFT_1_13,
|
||||
MINECRAFT_1_13_1,
|
||||
MINECRAFT_1_13_2,
|
||||
MINECRAFT_1_14,
|
||||
MINECRAFT_1_14_1,
|
||||
MINECRAFT_1_14_2,
|
||||
MINECRAFT_1_14_3,
|
||||
MINECRAFT_1_14_4,
|
||||
MINECRAFT_1_15,
|
||||
MINECRAFT_1_15_1,
|
||||
MINECRAFT_1_15_2,
|
||||
MINECRAFT_1_16,
|
||||
MINECRAFT_1_16_1,
|
||||
MINECRAFT_1_16_2,
|
||||
MINECRAFT_1_16_3,
|
||||
MINECRAFT_1_16_4,
|
||||
MINECRAFT_1_16_5,
|
||||
MINECRAFT_1_17,
|
||||
MINECRAFT_1_17_1,
|
||||
MINECRAFT_1_18,
|
||||
MINECRAFT_1_18_1,
|
||||
MINECRAFT_1_18_2,
|
||||
MINECRAFT_1_19,
|
||||
MINECRAFT_1_19_1,
|
||||
MINECRAFT_1_19_2,
|
||||
MINECRAFT_1_19_3,
|
||||
MINECRAFT_1_19_4,
|
||||
MINECRAFT_1_20,
|
||||
MINECRAFT_1_20_1,
|
||||
MINECRAFT_1_20_2,
|
||||
MINECRAFT_1_20_3,
|
||||
MINECRAFT_1_20_4,
|
||||
MINECRAFT_1_20_5,
|
||||
MINECRAFT_1_20_6,
|
||||
MINECRAFT_1_21;
|
||||
|
||||
static final BukkitServerVersion[] REVERSED_VALUES;
|
||||
|
||||
static {
|
||||
REVERSED_VALUES = values().clone();
|
||||
|
||||
final int size = REVERSED_VALUES.length;
|
||||
final int half = size / 2;
|
||||
|
||||
for (int i = 0; i < half; i++) {
|
||||
final BukkitServerVersion temp = REVERSED_VALUES[i];
|
||||
REVERSED_VALUES[i] = REVERSED_VALUES[size - 1 - i];
|
||||
REVERSED_VALUES[size - 1 - i] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
private final String release;
|
||||
|
||||
BukkitServerVersion() {
|
||||
this.release = name().substring(10).replace("_", ".");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.fallback;
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter {
|
||||
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
|
||||
for (final ChannelInboundHandler handler : handlers) {
|
||||
try {
|
||||
handler.channelInactive(proxy);
|
||||
} catch (Exception exception) {
|
||||
handler.exceptionCaught(proxy, exception);
|
||||
}
|
||||
}
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
public void add(final @NotNull ChannelHandler channelHandler) {
|
||||
if (channelHandler instanceof ChannelInboundHandler) {
|
||||
handlers.add((ChannelInboundHandler) channelHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class ProxiedChannelHandlerContext implements ChannelHandlerContext {
|
||||
private final ChannelHandlerContext ctx;
|
||||
|
||||
// Overwrite to prevent call this method on next handler
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelInactive() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireExceptionCaught(Throwable throwable) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel channel() {
|
||||
return ctx.channel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventExecutor executor() {
|
||||
return ctx.executor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ctx.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandler handler() {
|
||||
return ctx.handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoved() {
|
||||
return ctx.isRemoved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelRegistered() {
|
||||
return ctx.fireChannelRegistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelUnregistered() {
|
||||
return ctx.fireChannelUnregistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelActive() {
|
||||
return ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireUserEventTriggered(Object o) {
|
||||
return ctx.fireUserEventTriggered(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelRead(Object o) {
|
||||
return ctx.fireChannelRead(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelReadComplete() {
|
||||
return ctx.fireChannelReadComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelWritabilityChanged() {
|
||||
return ctx.fireChannelWritabilityChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture bind(SocketAddress socketAddress) {
|
||||
return ctx.bind(socketAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress) {
|
||||
return ctx.connect(socketAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1) {
|
||||
return ctx.connect(socketAddress, socketAddress1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture disconnect() {
|
||||
return ctx.disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture close() {
|
||||
return ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture deregister() {
|
||||
return ctx.deregister();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture bind(SocketAddress socketAddress, ChannelPromise channelPromise) {
|
||||
return ctx.bind(socketAddress, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress, ChannelPromise channelPromise) {
|
||||
return ctx.connect(socketAddress, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) {
|
||||
return ctx.connect(socketAddress, socketAddress1, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture disconnect(ChannelPromise channelPromise) {
|
||||
return ctx.disconnect(channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture close(ChannelPromise channelPromise) {
|
||||
return ctx.close(channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture deregister(ChannelPromise channelPromise) {
|
||||
return ctx.deregister(channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext read() {
|
||||
return ctx.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture write(Object o) {
|
||||
return ctx.write(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture write(Object o, ChannelPromise channelPromise) {
|
||||
return ctx.write(o, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext flush() {
|
||||
return ctx.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture writeAndFlush(Object o, ChannelPromise channelPromise) {
|
||||
return ctx.writeAndFlush(o, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture writeAndFlush(Object o) {
|
||||
return ctx.writeAndFlush(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPromise newPromise() {
|
||||
return ctx.newPromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelProgressivePromise newProgressivePromise() {
|
||||
return ctx.newProgressivePromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture newSucceededFuture() {
|
||||
return ctx.newSucceededFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture newFailedFuture(Throwable throwable) {
|
||||
return ctx.newFailedFuture(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPromise voidPromise() {
|
||||
return ctx.voidPromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipeline pipeline() {
|
||||
return ctx.pipeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufAllocator alloc() {
|
||||
return ctx.alloc();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public <T> Attribute<T> attr(AttributeKey<T> attributeKey) {
|
||||
return ctx.attr(attributeKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public <T> boolean hasAttr(AttributeKey<T> attributeKey) {
|
||||
return ctx.hasAttr(attributeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.fallback;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
|
||||
|
||||
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
|
||||
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
|
||||
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
FallbackBukkitInboundHandler() {
|
||||
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
|
||||
|
||||
channelRemovalListener = (pipeline, name, handler) -> {
|
||||
final ChannelInactiveListener inactiveListener = pipeline.get(ChannelInactiveListener.class);
|
||||
|
||||
if (inactiveListener != null) {
|
||||
inactiveListener.add(handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.registry = registry.getProtocolRegistry(FallbackPacketRegistry.Direction.SERVERBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
final ByteBuf byteBuf = (ByteBuf) msg;
|
||||
|
||||
/*
|
||||
* Reset the reader index, so we can read the content;
|
||||
* We also have to make sure to store the original reader index.
|
||||
* We do this to 'spoof' the packet state - we want to read it without the server knowing.
|
||||
*/
|
||||
final int originalReaderIndex = byteBuf.readerIndex();
|
||||
byteBuf.readerIndex(0);
|
||||
|
||||
// Don't continue if the ByteBuf isn't readable or the channel is closed
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
final int packetId = ProtocolUtil.readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// Skip the packet if it's not registered within Sonar's packet registry
|
||||
if (packet == null) {
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to decode the packet for the given protocol version
|
||||
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
|
||||
} catch (Throwable throwable) {
|
||||
byteBuf.release();
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
// Check if the packet still has bytes left after we decoded it
|
||||
if (byteBuf.isReadable()) {
|
||||
byteBuf.release();
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Could not read packet to end ("
|
||||
+ byteBuf.readableBytes() + " bytes left)") : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Useful resources:
|
||||
* https://wiki.vg/Protocol#Handshaking
|
||||
* https://wiki.vg/Protocol#Login
|
||||
*/
|
||||
if (packet instanceof HandshakePacket) {
|
||||
final HandshakePacket handshake = (HandshakePacket) packet;
|
||||
switch (handshake.getIntent()) {
|
||||
case STATUS:
|
||||
// We don't care about server pings or transfers; remove the handler
|
||||
ctx.pipeline().remove(this);
|
||||
break;
|
||||
case LOGIN:
|
||||
case TRANSFER:
|
||||
// Let the actual handler know about the handshake packet
|
||||
handleHandshake(ctx, handshake.getHostname(), handshake.getProtocolVersionId());
|
||||
// Be ready for the next packet (which is supposed to be a login packet)
|
||||
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
|
||||
break;
|
||||
default:
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Bad handshake intent " + handshake.getIntent())
|
||||
: QuietDecoderException.INSTANCE;
|
||||
}
|
||||
} else if (packet instanceof LoginStartPacket) {
|
||||
final LoginStartPacket loginStart = (LoginStartPacket) packet;
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
|
||||
// We've done our job - deject this pipeline
|
||||
ctx.pipeline().remove(this);
|
||||
// Let Sonar process the login packet
|
||||
handleLogin(ctx, () -> {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
ctx.fireChannelRead(byteBuf.retain());
|
||||
// TODO: recode this?
|
||||
final ChannelHandler inboundHandler = ctx.pipeline().remove(FALLBACK_INBOUND_HANDLER);
|
||||
if (inboundHandler != null) {
|
||||
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
|
||||
}
|
||||
}, loginStart.getUsername(), socketAddress);
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
ctx.fireChannelRead(byteBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
|
||||
import xyz.jonesdev.sonar.common.util.exception.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.bukkit.SonarBukkit.INITIALIZE_LISTENER;
|
||||
|
||||
// Check out these links if you want to see some more magic
|
||||
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
|
||||
// https://github.com/dmulloy2/ProtocolLib/blob/master/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java
|
||||
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/handlers/BukkitChannelInitializer.java
|
||||
@UtilityClass
|
||||
public class FallbackBukkitInjector {
|
||||
private final String LEGACY_NMS_PACKAGE;
|
||||
private final String OBC_PACKAGE;
|
||||
|
||||
private final BukkitServerVersion SERVER_VERSION;
|
||||
private final boolean USES_LEGACY_PACKAGING, OBFUSCATED;
|
||||
|
||||
private final Class<?> MINECRAFT_SERVER_CLASS;
|
||||
private final Class<?> CRAFTBUKKIT_SERVER_CLASS;
|
||||
private final Class<?> SERVER_CONNECTION_CLASS;
|
||||
private final Object SERVER_INSTANCE;
|
||||
|
||||
@Getter
|
||||
private boolean lateBindEnabled;
|
||||
|
||||
static {
|
||||
SERVER_VERSION = resolveServerVersion();
|
||||
USES_LEGACY_PACKAGING = SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_17) < 0;
|
||||
|
||||
try {
|
||||
final String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
String modifiedPackageName;
|
||||
if (SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_20_5) >= 0) {
|
||||
// Example: org.bukkit.craftbukkit
|
||||
modifiedPackageName = bukkitPackage.split("\\.")[2];
|
||||
} else {
|
||||
// Example: org.bukkit.craftbukkit.v1_16_R3
|
||||
modifiedPackageName = bukkitPackage.split("\\.")[3];
|
||||
}
|
||||
|
||||
LEGACY_NMS_PACKAGE = "net.minecraft.server." + modifiedPackageName + ".";
|
||||
OBC_PACKAGE = bukkitPackage + ".";
|
||||
|
||||
OBFUSCATED = isObfuscated();
|
||||
// Minecraft classes
|
||||
MINECRAFT_SERVER_CLASS = getNMSClass("server.MinecraftServer", "MinecraftServer");
|
||||
SERVER_CONNECTION_CLASS = getNMSClass(OBFUSCATED ? "server.network.ServerConnection" : "server.network.ServerConnectionListener", "ServerConnection");
|
||||
// CraftBukkit classes
|
||||
CRAFTBUKKIT_SERVER_CLASS = getOBCClass("CraftServer");
|
||||
|
||||
Object minecraftServerInstance;
|
||||
try {
|
||||
// 1.20.5+
|
||||
minecraftServerInstance = getFieldAt(MINECRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(null);
|
||||
} catch (Exception exception) {
|
||||
minecraftServerInstance = getFieldAt(CRAFTBUKKIT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(Bukkit.getServer());
|
||||
}
|
||||
SERVER_INSTANCE = minecraftServerInstance;
|
||||
|
||||
checkLateBind();
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLateBind() {
|
||||
try {
|
||||
try {
|
||||
Class.forName("org.bukkit.event.server.ServerLoadEvent");
|
||||
// late-bind does not exist on these versions, ignore this case
|
||||
return;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
|
||||
final Class<?> spigotConfiguration = Class.forName("org.spigotmc.SpigotConfig");
|
||||
final Method initFieldMethod = spigotConfiguration.getDeclaredMethod("lateBind");
|
||||
initFieldMethod.setAccessible(true);
|
||||
initFieldMethod.invoke(null);
|
||||
|
||||
if ((boolean) spigotConfiguration.getField("lateBind").get(null)) {
|
||||
lateBindEnabled = true;
|
||||
}
|
||||
} catch (java.lang.ReflectiveOperationException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull Field getFieldAt(final @NotNull Class<?> clazz,
|
||||
final @NotNull Class<?> type,
|
||||
final int index) {
|
||||
int currentIndex = 0;
|
||||
for (final Field field : clazz.getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
|
||||
if (type.isAssignableFrom(field.getType())) {
|
||||
if (currentIndex++ == index) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clazz.getSuperclass() != null) {
|
||||
return getFieldAt(clazz.getSuperclass(), type, index);
|
||||
}
|
||||
throw new IllegalStateException("Could not find field #" + index + " in " + clazz.getName());
|
||||
}
|
||||
|
||||
public static @NotNull Field findField(final boolean checkSuperClass,
|
||||
final @NotNull Class<?> clazz,
|
||||
final String @NotNull ... types) throws NoSuchFieldException {
|
||||
for (final Field field : clazz.getDeclaredFields()) {
|
||||
final String fieldTypeName = field.getType().getSimpleName();
|
||||
for (final String type : types) {
|
||||
if (fieldTypeName.equals(type)) {
|
||||
if (!Modifier.isPublic(field.getModifiers())) {
|
||||
field.setAccessible(true);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checkSuperClass && clazz != Object.class && clazz.getSuperclass() != null) {
|
||||
return findField(true, clazz.getSuperclass(), types);
|
||||
}
|
||||
throw new NoSuchFieldException(types[0]);
|
||||
}
|
||||
|
||||
private @NotNull BukkitServerVersion resolveServerVersion() {
|
||||
final String bukkitVersion = Bukkit.getBukkitVersion();
|
||||
|
||||
for (final BukkitServerVersion serverVersion : BukkitServerVersion.REVERSED_VALUES) {
|
||||
if (bukkitVersion.contains(serverVersion.getRelease())) {
|
||||
return serverVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an error if we cannot find the server version
|
||||
throw new IllegalStateException("Could not find server version " + bukkitVersion);
|
||||
}
|
||||
|
||||
private boolean isObfuscated() {
|
||||
try {
|
||||
Class.forName("net.minecraft.server.network.PlayerConnection");
|
||||
return true;
|
||||
} catch (ClassNotFoundException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Class<?> getNMSClass(final String modern, final String legacy) throws ClassNotFoundException {
|
||||
return Class.forName(USES_LEGACY_PACKAGING ? LEGACY_NMS_PACKAGE + legacy : "net.minecraft." + modern);
|
||||
}
|
||||
|
||||
public @NotNull Class<?> getOBCClass(final String clazz) throws ClassNotFoundException {
|
||||
return Class.forName(OBC_PACKAGE + clazz);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void inject() {
|
||||
try {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
|
||||
|
||||
// Check if the field has the correct generic type; We need this field to be List<ChannelFuture>
|
||||
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final var list = (List<?>) field.get(getFieldAt(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, 0).get(SERVER_INSTANCE));
|
||||
|
||||
for (final Object object : list) {
|
||||
final ChannelFuture channelFuture = (ChannelFuture) object;
|
||||
final List<String> names = channelFuture.channel().pipeline().names();
|
||||
|
||||
ChannelHandler bootstrap = null;
|
||||
|
||||
for (final String name : names) {
|
||||
try {
|
||||
final ChannelHandler handler = channelFuture.channel().pipeline().get(name);
|
||||
final Field childHandlerField = handler.getClass().getDeclaredField("childHandler");
|
||||
childHandlerField.setAccessible(true);
|
||||
final var childHandler = (ChannelInitializer<?>) childHandlerField.get(handler);
|
||||
|
||||
if (childHandler != null) {
|
||||
bootstrap = handler;
|
||||
}
|
||||
break;
|
||||
} catch (Exception exception) {
|
||||
// Some plugins modify this, so we better skip any exceptions
|
||||
}
|
||||
}
|
||||
|
||||
if (bootstrap == null) {
|
||||
// Default to the first channel future if we couldn't find a childHandler
|
||||
bootstrap = channelFuture.channel().pipeline().first();
|
||||
}
|
||||
|
||||
final Field childHandlerField = bootstrap.getClass().getDeclaredField("childHandler");
|
||||
childHandlerField.setAccessible(true);
|
||||
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
|
||||
|
||||
/*
|
||||
* Inject our own channel initializer into the original field
|
||||
* If the plugin is not fully enabled but executes the injection,
|
||||
* put an initializer to close the new connection automatically.
|
||||
*/
|
||||
if (!INITIALIZE_LISTENER.isDone()) {
|
||||
childHandlerField.set(bootstrap, new ChannelInitializer<>() {
|
||||
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) {
|
||||
channel.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final ChannelHandler _bootstrap = bootstrap;
|
||||
|
||||
INITIALIZE_LISTENER.thenAccept(__ -> {
|
||||
try {
|
||||
childHandlerField.set(_bootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
|
||||
pipeline -> {
|
||||
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
|
||||
pipeline.addFirst(FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
|
||||
}
|
||||
));
|
||||
} catch (IllegalAccessException exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.listener;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
|
||||
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
|
||||
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
|
||||
public final class BukkitJoinListener implements Listener {
|
||||
private static MethodHandle _handleMethod;
|
||||
// ServerGamePacketListenerImpl aka. PlayerConnection
|
||||
private static MethodHandle _listenerField;
|
||||
private static MethodHandle _networkManagerField;
|
||||
private static MethodHandle _channelField;
|
||||
|
||||
private static boolean initialized;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Method handleMethod = FallbackBukkitInjector.getOBCClass("entity.CraftPlayer")
|
||||
.getDeclaredMethod("getHandle");
|
||||
final Field listenerField = FallbackBukkitInjector.findField(false, handleMethod.getReturnType(),
|
||||
"PlayerConnection", "ServerGamePacketListenerImpl");
|
||||
final Field networkManagerField = FallbackBukkitInjector.findField(true, listenerField.getType(),
|
||||
"NetworkManager", "Connection");
|
||||
final Field channelField = FallbackBukkitInjector.getFieldAt(networkManagerField.getType(), Channel.class, 0);
|
||||
|
||||
_handleMethod = MethodHandles.lookup().unreflect(handleMethod);
|
||||
_listenerField = MethodHandles.lookup().unreflectGetter(listenerField);
|
||||
_networkManagerField = MethodHandles.lookup().unreflectGetter(networkManagerField);
|
||||
_channelField = MethodHandles.lookup().unreflectGetter(channelField);
|
||||
initialized = true;
|
||||
} catch (Exception exception) {
|
||||
initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
// Make sure the player is not an NPC
|
||||
if (!initialized || event.getPlayer().hasMetadata("NPC")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Object handle = _handleMethod.invoke(event.getPlayer());
|
||||
final Object listener = _listenerField.invoke(handle);
|
||||
final Object networkManager = _networkManagerField.invoke(listener);
|
||||
final Channel channel = (Channel) _channelField.invoke(networkManager);
|
||||
|
||||
// Don't continue if the channel isn't found or if the channel is inactive
|
||||
if (channel == null || !channel.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player is not a fake player
|
||||
if (FakeChannelUtil.isFakeChannel(channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the channel if Sonar's handlers are not found when the PlayerJoinEvent is called
|
||||
if (channel.pipeline().context(FALLBACK_BANDWIDTH) == null) {
|
||||
channel.close();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().warn("Couldn't find {}: {}", event.getPlayer().getName(), throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
plugins {
|
||||
id("net.minecrell.plugin-yml.bungee") version "0.6.0" apply true
|
||||
alias(libs.plugins.pluginyml.bungee) apply true
|
||||
}
|
||||
|
||||
bungee {
|
||||
name = rootProject.name
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
|
||||
@@ -18,10 +16,9 @@ dependencies {
|
||||
compileOnly(rootProject.libs.bungeecord)
|
||||
testCompileOnly(rootProject.libs.bungeecord)
|
||||
|
||||
implementation(rootProject.libs.adventure.platform.bungee)
|
||||
implementation(rootProject.libs.adventure.platform.api)
|
||||
implementation(rootProject.libs.adventure.platform.facet)
|
||||
implementation(rootProject.libs.adventure.nbt)
|
||||
implementation(rootProject.libs.adventure.platform.bungee) {
|
||||
exclude(module = "adventure-nbt")
|
||||
}
|
||||
implementation(rootProject.libs.bstats.bungee)
|
||||
implementation(rootProject.libs.libby.bungee)
|
||||
}
|
||||
@@ -29,6 +26,8 @@ dependencies {
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
|
||||
archiveFileName = "Sonar-Bungee.jar" // Hardcode a shortened version of the name
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -21,6 +21,7 @@ import com.alessiodp.libby.BungeeLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -35,11 +36,8 @@ import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +53,11 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
return bungeeAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Audience sender(final @NotNull Object object) {
|
||||
return bungeeAudiences.sender((CommandSender) object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -91,6 +94,10 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
() -> getConfig().getVerification().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("captcha",
|
||||
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("language",
|
||||
() -> getConfig().getLanguage().getName()));
|
||||
metrics.addCustomChart(new SimplePie("database_type",
|
||||
() -> getConfig().getDatabase().getType().getDisplayName()));
|
||||
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
@@ -101,8 +108,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Make sure to properly stop the metrics
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
+5
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -22,11 +22,11 @@ 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 org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import java.util.Collections;
|
||||
|
||||
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
|
||||
public BungeeSonarCommand() {
|
||||
@@ -38,7 +38,7 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
Sonar.get().sender(sender),
|
||||
sender::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
@@ -47,9 +47,6 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
return getCachedTabSuggestions(args);
|
||||
return sender.hasPermission("sonar.command") ? getCachedTabSuggestions(args) : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
+32
-31
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
@@ -26,20 +25,16 @@ import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Handshake;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.util.exception.ReflectiveOperationException;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.STATUS;
|
||||
|
||||
public final class FallbackBungeeChannelHandler extends FallbackChannelHandlerAdapter {
|
||||
|
||||
public FallbackBungeeChannelHandler(final @NotNull Channel channel) {
|
||||
super(channel);
|
||||
}
|
||||
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
|
||||
|
||||
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
|
||||
|
||||
@@ -54,38 +49,44 @@ public final class FallbackBungeeChannelHandler extends FallbackChannelHandlerAd
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// TODO: put this into a separate handler
|
||||
// Intercept any packets processed by BungeeCord
|
||||
if (listenForPackets && msg instanceof PacketWrapper) {
|
||||
if (msg instanceof PacketWrapper) {
|
||||
final PacketWrapper packetWrapper = (PacketWrapper) msg;
|
||||
final DefinedPacket wrappedPacket = packetWrapper.packet;
|
||||
// Don't handle any invalid packets
|
||||
// Skip any unknown or invalid packets
|
||||
if (wrappedPacket != null) {
|
||||
// Intercept any handshake packet by the client
|
||||
if (wrappedPacket instanceof Handshake) {
|
||||
final Handshake handshake = (Handshake) wrappedPacket;
|
||||
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
else if (wrappedPacket instanceof LoginRequest) {
|
||||
// We don't care about server pings; remove the handler
|
||||
if (handshake.getRequestedProtocol() == STATUS) {
|
||||
ctx.pipeline().remove(this);
|
||||
} else {
|
||||
handleHandshake(ctx, handshake.getHost(), handshake.getProtocolVersion());
|
||||
}
|
||||
} else if (wrappedPacket instanceof LoginRequest) {
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper;
|
||||
try {
|
||||
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
|
||||
} catch (final Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
final HandlerBoss handlerBoss = ctx.pipeline().get(HandlerBoss.class);
|
||||
// This might sometimes happen if the client unexpectedly disconnects
|
||||
if (handlerBoss != null) {
|
||||
// Get the real IP address of the player through the channel wrapper
|
||||
final ChannelWrapper channelWrapper;
|
||||
try {
|
||||
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
|
||||
} catch (final Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
// We've done our job - deject this pipeline
|
||||
ctx.pipeline().remove(this);
|
||||
// Make sure to mark this packet as released
|
||||
packetWrapper.trySingleRelease();
|
||||
// Let Sonar process the login packet
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
handleLogin(ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
|
||||
return;
|
||||
}
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
|
||||
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
packetWrapper.trySingleRelease();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
+10
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -22,13 +22,13 @@ import io.netty.channel.ChannelInitializer;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import sun.misc.Unsafe;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.common.fallback.injection.FallbackInjectedChannelInitializer;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackBungeeInjector {
|
||||
@@ -39,23 +39,21 @@ public class FallbackBungeeInjector {
|
||||
|
||||
// Make sure to store the original channel initializer
|
||||
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
|
||||
final ChannelInitializer<Channel> fallbackInitializer = new FallbackInjectedChannelInitializer(
|
||||
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_HANDLER,
|
||||
new FallbackBungeeChannelHandler(pipeline.channel())));
|
||||
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
|
||||
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
|
||||
new FallbackBungeeInboundHandler()));
|
||||
|
||||
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
|
||||
|
||||
// Get the base object (the object containing the field)
|
||||
final Object base = unsafe.staticFieldBase(childField);
|
||||
// Get the offset of the static field within its class
|
||||
final long offset = unsafe.staticFieldOffset(childField);
|
||||
|
||||
// Replace the original channel initializer
|
||||
unsafe.putObject(base, offset, fallbackInitializer);
|
||||
// Replace the original channel initializer with our new injected initializer
|
||||
unsafe.putObject(base, offset, injectedInitializer);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(rootProject.libs.imagefilters)
|
||||
|
||||
testCompileOnly(project(":api"))
|
||||
testCompileOnly(rootProject.libs.imagefilters)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.captcha;
|
||||
|
||||
import com.jhlabs.image.FBMFilter;
|
||||
import com.jhlabs.image.SmearFilter;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
|
||||
import xyz.jonesdev.sonar.captcha.filters.CurvesOverlayFilter;
|
||||
import xyz.jonesdev.sonar.captcha.filters.NoiseOverlayFilter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.font.FontRenderContext;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class StandardCaptchaGenerator implements CaptchaGenerator {
|
||||
private static final CurvesOverlayFilter CURVES = new CurvesOverlayFilter(3);
|
||||
//private static final CircleInverseFilter CIRCLES = new CircleInverseFilter(1, 30, 10);
|
||||
private static final NoiseOverlayFilter NOISE = new NoiseOverlayFilter(1, 20);
|
||||
private static final SmearFilter SMEAR = new SmearFilter();
|
||||
private static final FBMFilter FBM = new FBMFilter();
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final Color[] COLORS = new Color[4];
|
||||
private static final float[] COLOR_FRACTIONS = new float[COLORS.length];
|
||||
|
||||
static {
|
||||
FBM.setAmount(0.6f);
|
||||
FBM.setScale(15);
|
||||
|
||||
SMEAR.setShape(SmearFilter.SQUARES);
|
||||
SMEAR.setDensity(0.1f);
|
||||
SMEAR.setDistance(0);
|
||||
SMEAR.setMix(0.35f);
|
||||
|
||||
// Create fractions based on the number of colors
|
||||
for (int i = 0; i < COLOR_FRACTIONS.length; i++) {
|
||||
COLOR_FRACTIONS[i] = (float) i / (COLOR_FRACTIONS.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private final int width = 128, height = 128;
|
||||
private final @Nullable File background;
|
||||
private @Nullable BufferedImage backgroundImage;
|
||||
|
||||
@Override
|
||||
public @NotNull BufferedImage createImage(final char @NotNull [] answer) {
|
||||
final BufferedImage image = createBackgroundImage();
|
||||
final Graphics2D graphics = image.createGraphics();
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
// Draw characters and other effects on the image
|
||||
applyRandomColorGradient(graphics);
|
||||
drawCharacters(graphics, answer);
|
||||
CURVES.transform(image, graphics);
|
||||
NOISE.transform(image);
|
||||
//SMEAR.filter(image, image);
|
||||
//CIRCLES.transform(image); // TODO: check if the text is still easy to read after this
|
||||
// Make sure to dispose the graphics instance
|
||||
graphics.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
private @NotNull BufferedImage createBackgroundImage() {
|
||||
// Create a new image buffer with a 3-byte color spectrum
|
||||
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
|
||||
if (background == null) {
|
||||
// Fill the entire image with a noise texture
|
||||
return FBM.filter(image, image);
|
||||
}
|
||||
// Try to load the image from a file if it doesn't exist yet
|
||||
if (backgroundImage == null) {
|
||||
try {
|
||||
backgroundImage = ImageIO.read(background);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Could not read background image", exception);
|
||||
}
|
||||
}
|
||||
image.createGraphics().drawImage(backgroundImage, 0, 0, Color.WHITE, null);
|
||||
return image;
|
||||
}
|
||||
|
||||
private void applyRandomColorGradient(final @NotNull Graphics2D graphics) {
|
||||
// Randomize the colors for the gradient effect
|
||||
for (int i = 0; i < COLORS.length; i++) {
|
||||
final float random = 0.9f + RANDOM.nextFloat() * 0.1f;
|
||||
COLORS[i] = Color.getHSBColor(RANDOM.nextFloat(), random, random);
|
||||
}
|
||||
|
||||
// Apply the random gradient effect
|
||||
graphics.setPaint(new LinearGradientPaint(0, 0, width, height,
|
||||
COLOR_FRACTIONS, COLORS, MultipleGradientPaint.CycleMethod.REFLECT));
|
||||
}
|
||||
|
||||
private void drawCharacters(final @NotNull Graphics2D graphics,
|
||||
final char @NotNull [] answer) {
|
||||
final FontRenderContext ctx = graphics.getFontRenderContext();
|
||||
final GlyphVector[] glyphs = new GlyphVector[answer.length];
|
||||
|
||||
for (int i = 0; i < answer.length; i++) {
|
||||
// Create a glyph vector for the character with a random font
|
||||
final Font font = StandardTTFFontProvider.FONTS[RANDOM.nextInt(StandardTTFFontProvider.FONTS.length)];
|
||||
glyphs[i] = font.createGlyphVector(ctx, String.valueOf(answer[i]));
|
||||
}
|
||||
|
||||
final double scalingXY = 5 - Math.min(answer.length, 5) * 0.65;
|
||||
|
||||
// Calculate first X and Y positions
|
||||
final double totalWidth = Arrays.stream(glyphs)
|
||||
.mapToDouble(glyph -> glyph.getLogicalBounds().getWidth() * scalingXY - 1)
|
||||
.sum();
|
||||
double beginX = Math.max(Math.min(width / 2D - totalWidth / 2D, totalWidth), 0);
|
||||
double beginY = (height + StandardTTFFontProvider.STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
|
||||
|
||||
// Draw each glyph one by one
|
||||
for (final GlyphVector glyph : glyphs) {
|
||||
final AffineTransform transformation = AffineTransform.getTranslateInstance(beginX, beginY);
|
||||
// Shear the glyph by a random amount
|
||||
final double shearXY = Math.sin(beginX + beginY) / 6;
|
||||
transformation.shear(shearXY, shearXY);
|
||||
// Scale the glyph to perfectly fit the image
|
||||
transformation.scale(scalingXY, scalingXY);
|
||||
// Draw the glyph to the buffered image
|
||||
final Shape transformedShape = transformation.createTransformedShape(glyph.getOutline());
|
||||
graphics.fill(transformedShape);
|
||||
// Draw a random outline around the glyph
|
||||
if (RANDOM.nextFloat() < 0.25f) {
|
||||
createGlyphOutline(graphics, transformedShape);
|
||||
}
|
||||
// Make sure the next glyph isn't drawn at the same position
|
||||
beginX += glyph.getVisualBounds().getWidth() * scalingXY + 2;
|
||||
beginY += -10 + RANDOM.nextFloat() * 20;
|
||||
}
|
||||
}
|
||||
|
||||
private void createGlyphOutline(final @NotNull Graphics2D graphics, final @NotNull Shape shape) {
|
||||
final float txy = 1.25f + RANDOM.nextFloat();
|
||||
final float width = 1 + RANDOM.nextFloat();
|
||||
|
||||
// Create a randomly translated and stroked shape based on the original glyph shape
|
||||
final AffineTransform translation = AffineTransform.getTranslateInstance(txy, txy);
|
||||
final Shape strokedShape = new BasicStroke(width).createStrokedShape(shape);
|
||||
|
||||
graphics.fill(translation.createTransformedShape(strokedShape));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.captcha;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
@UtilityClass
|
||||
class StandardTTFFontProvider {
|
||||
private static final String[] FONT_NAMES = {"Kingthings_Trypewriter_2"};
|
||||
static final Font[] FONTS = new Font[FONT_NAMES.length];
|
||||
static final int STANDARD_FONT_SIZE = 25;
|
||||
|
||||
static {
|
||||
for (int i = 0; i < FONT_NAMES.length; i++) {
|
||||
FONTS[i] = loadFont(String.format("/assets/fonts/%s.ttf", FONT_NAMES[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private static Font loadFont(final @NotNull String path) {
|
||||
try (final InputStream inputStream = StandardCaptchaGenerator.class.getResourceAsStream(path)) {
|
||||
// Load the font from the TTF file
|
||||
final Font customFont = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(inputStream));
|
||||
// Set the font size and style
|
||||
return customFont.deriveFont(Font.PLAIN, STANDARD_FONT_SIZE);
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class CircleInverseFilter {
|
||||
private final int amount, minRadius, maxRadiusExpansion;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public void transform(final @NotNull BufferedImage image) {
|
||||
final int minX = image.getWidth() / 4;
|
||||
final int minY = image.getHeight() / 4;
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
// Generate random position and radius for the circle
|
||||
final float centerX = minX + RANDOM.nextInt(image.getWidth() - minX * 2);
|
||||
final float centerY = minY + RANDOM.nextInt(image.getHeight() - minY * 2);
|
||||
final int radius = minRadius + RANDOM.nextInt(maxRadiusExpansion);
|
||||
|
||||
final Circle circle = new Circle(centerX, centerY, radius);
|
||||
// Draw inverted circle
|
||||
_addInvertedCircle(image, circle);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _addInvertedCircle(final @NotNull BufferedImage bufferedImage,
|
||||
final @NotNull Circle circle) {
|
||||
for (int x = 0; x < bufferedImage.getWidth(); x++) {
|
||||
for (int y = 0; y < bufferedImage.getHeight(); y++) {
|
||||
if (isWithinCircle(x, y, circle, circle.radius)) {
|
||||
// Invert colors within the circle
|
||||
final int rgb = bufferedImage.getRGB(x, y);
|
||||
final int invertedRGB = invertColorAndFilter(rgb);
|
||||
bufferedImage.setRGB(x, y, invertedRGB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isWithinCircle(final float x, final float y, final @NotNull Circle circle, int radius) {
|
||||
final float distanceX = x - circle.centerX;
|
||||
final float distanceY = y - circle.centerY;
|
||||
return distanceX * distanceX + distanceY * distanceY <= radius * radius;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
static final class Circle {
|
||||
private final float centerX;
|
||||
private final float centerY;
|
||||
private final int radius;
|
||||
}
|
||||
|
||||
private static int invertColorAndFilter(final int rgb) {
|
||||
final Color color = new Color(rgb);
|
||||
final int red = Math.max(Color.LIGHT_GRAY.getRed() - color.getRed(), 0);
|
||||
final int green = Math.max(Color.LIGHT_GRAY.getGreen() - color.getGreen(), 0);
|
||||
final int blue = Math.max(Color.LIGHT_GRAY.getBlue() - color.getBlue(), 0);
|
||||
return new Color(red, green, blue).getRGB();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class CurvesOverlayFilter {
|
||||
private final int amount;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public void transform(final @NotNull BufferedImage image,
|
||||
final @NotNull Graphics2D graphics) {
|
||||
// Randomize the stroke width
|
||||
graphics.setStroke(new BasicStroke(1 + RANDOM.nextFloat()));
|
||||
|
||||
final int halfWidth = image.getWidth() / 2;
|
||||
|
||||
for (int i = 0; i < amount; ++i) {
|
||||
final float randomX = image.getWidth() * RANDOM.nextFloat();
|
||||
final float randomY = image.getHeight() * RANDOM.nextFloat();
|
||||
final float amplitude = 6.2831855f * (RANDOM.nextFloat() - 0.5f);
|
||||
final float sin = (float) Math.sin(amplitude) * halfWidth;
|
||||
final float cos = (float) Math.cos(amplitude) * halfWidth;
|
||||
final float x1 = randomX - cos;
|
||||
final float y1 = randomY - sin;
|
||||
final float x2 = randomX + cos;
|
||||
final float y2 = randomY + sin;
|
||||
// Control points for the cubic curve
|
||||
final float ctrlX1 = randomX + sin / 2;
|
||||
final float ctrlY1 = randomY - cos / 2;
|
||||
final float ctrlX2 = randomX - sin / 2;
|
||||
final float ctrlY2 = randomY + cos / 2;
|
||||
// Draw a cubic curve instead of a straight line
|
||||
graphics.draw(new CubicCurve2D.Float(x1, y1, ctrlX1, ctrlY1, ctrlX2, ctrlY2, x2, y2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class GridOverlayFilter {
|
||||
private final int amount, lineWidth, randomOffset;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public void transform(final @NotNull BufferedImage image,
|
||||
final @NotNull Graphics2D graphics) {
|
||||
final int spacingX = image.getWidth() / amount + lineWidth;
|
||||
final int spacingY = image.getHeight() / amount + lineWidth;
|
||||
int currentX = spacingX / 2;
|
||||
int currentY = spacingY / 2;
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
graphics.setStroke(new BasicStroke(lineWidth * 0.5f + lineWidth * RANDOM.nextFloat()));
|
||||
graphics.drawLine(currentX, 0, currentX + RANDOM.nextInt(randomOffset), image.getHeight());
|
||||
graphics.drawLine(0, currentY, image.getWidth(), currentY + RANDOM.nextInt(randomOffset));
|
||||
|
||||
currentX += spacingX + lineWidth;
|
||||
currentY += spacingY + lineWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class NoiseOverlayFilter {
|
||||
private final float density, amount;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private int randomGaussian(int x) {
|
||||
x += (int) (RANDOM.nextGaussian() * amount);
|
||||
return Math.max(Math.min(x, 0xff), 0);
|
||||
}
|
||||
|
||||
public void transform(final @NotNull BufferedImage image) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
if (RANDOM.nextFloat() <= density) {
|
||||
final int rgb = image.getRGB(x, y);
|
||||
int r = (rgb >> 16) & 0xff;
|
||||
int g = (rgb >> 8) & 0xff;
|
||||
int b = rgb & 0xff;
|
||||
r = randomGaussian(r);
|
||||
g = randomGaussian(g);
|
||||
b = randomGaussian(b);
|
||||
image.setRGB(x, y, (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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/>.
|
||||
*/
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
@UtilityClass
|
||||
public class Main {
|
||||
public void main(final String... args) throws IOException {
|
||||
final Random random = new Random();
|
||||
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(null);
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
final char[] dictionary = {'a', 'b', 'c', 'd', 'e', 'f'/*, 'g'*/, 'h'/*, 'i'*/, 'j',
|
||||
'k', 'm', 'n', 'o', 'p'/*, 'q'*/, 'r', 's', 't', 'u'/*, 'v', 'w'*/, 'x', 'y', 'z'};
|
||||
|
||||
// Generate image
|
||||
final int amount = 1;
|
||||
for (int i = 0; i < amount; i++) {
|
||||
final char[] answer = new char[3 + random.nextInt(2)];
|
||||
for (int j = 0; j < answer.length; j++) {
|
||||
answer[j] = dictionary[random.nextInt(dictionary.length)];
|
||||
}
|
||||
final BufferedImage bufferedImage = standardCaptchaGenerator.createImage(answer);
|
||||
|
||||
// Save image
|
||||
ImageIO.write(bufferedImage, "png", new File(i + ".png"));
|
||||
}
|
||||
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to generate " + amount + " image(s)");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
|
||||
implementation(project(":captcha"))
|
||||
compileOnly(rootProject.libs.adventure.nbt)
|
||||
|
||||
implementation(rootProject.libs.capja)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
+13
-21
@@ -40,12 +40,12 @@ class LibraryLoader {
|
||||
Library.builder()
|
||||
.groupId("com{}google{}code{}gson")
|
||||
.artifactId("gson")
|
||||
.version("2.10.1")
|
||||
.version("2.11.0")
|
||||
.relocate("com{}google{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
|
||||
.build(),
|
||||
// Caffeine
|
||||
Library.builder()
|
||||
.groupId("com.github.ben-manes.caffeine")
|
||||
.groupId("com{}github{}ben-manes{}caffeine")
|
||||
.artifactId("caffeine")
|
||||
.version("3.1.8")
|
||||
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
|
||||
@@ -57,26 +57,12 @@ class LibraryLoader {
|
||||
.version("6.1")
|
||||
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
|
||||
.build(),
|
||||
// MySQL driver
|
||||
// JHLabs image filters
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("8.4.0")
|
||||
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
|
||||
.build(),
|
||||
// MariaDB driver
|
||||
Library.builder()
|
||||
.groupId("org{}mariadb{}jdbc")
|
||||
.artifactId("mariadb-java-client")
|
||||
.version("3.4.0")
|
||||
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
|
||||
.build(),
|
||||
// H2 driver
|
||||
Library.builder()
|
||||
.groupId("com{}h2database")
|
||||
.artifactId("h2")
|
||||
.version("2.1.214")
|
||||
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
|
||||
.groupId("com{}jhlabs")
|
||||
.artifactId("filters")
|
||||
.version("2.0.235-1")
|
||||
.relocate("com{}jhlabs", "xyz{}jonesdev{}sonar{}libs{}jhlabs")
|
||||
.build()
|
||||
);
|
||||
|
||||
@@ -94,6 +80,12 @@ class LibraryLoader {
|
||||
.artifactId("adventure-text-serializer-gson")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build(),
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-nbt")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
+25
-37
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -31,16 +31,18 @@ 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;
|
||||
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
|
||||
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
|
||||
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
|
||||
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
|
||||
import xyz.jonesdev.sonar.common.fallback.ratelimit.CaffeineCacheRatelimiter;
|
||||
import xyz.jonesdev.sonar.common.fallback.ratelimit.NoopCacheRatelimiter;
|
||||
import xyz.jonesdev.sonar.common.service.ScheduledServiceManager;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.subcommands.*;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
import xyz.jonesdev.sonar.common.subcommand.*;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
@@ -50,11 +52,12 @@ import java.time.Duration;
|
||||
public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private T plugin;
|
||||
@Setter
|
||||
private Verbose verboseHandler;
|
||||
private ActionBarNotificationHandler actionBarNotificationHandler;
|
||||
@Setter
|
||||
private Notification notificationHandler;
|
||||
private ChatNotificationHandler chatNotificationHandler;
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private final LibraryManager libraryManager;
|
||||
private final SonarStatistics statistics;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
@@ -66,6 +69,9 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
final @NotNull LibraryManager libraryManager) {
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager, platform);
|
||||
this.libraryManager = libraryManager;
|
||||
// Check if Netty is up to date
|
||||
ProtocolUtil.checkNettyVersion();
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
// Set the plugin instance
|
||||
@@ -73,11 +79,11 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
this.platform = platform;
|
||||
// Load the rest of the components
|
||||
this.statistics = new GlobalSonarStatistics();
|
||||
this.verboseHandler = new Verbose();
|
||||
this.notificationHandler = new Notification();
|
||||
this.actionBarNotificationHandler = new ActionBarNotificationHandler();
|
||||
this.chatNotificationHandler = new ChatNotificationHandler();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistry();
|
||||
// Register all subcommands
|
||||
this.subcommandRegistry = new SubcommandRegistry();
|
||||
this.subcommandRegistry.register(
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
@@ -91,13 +97,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
}
|
||||
|
||||
public final void initialize() {
|
||||
// Check if the branch is not the main branch to warn about unstable versions
|
||||
if (!getVersion().getGitBranch().equals("main")) {
|
||||
getLogger().warn("You are currently using an unofficial experimental branch.");
|
||||
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
|
||||
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
|
||||
}
|
||||
|
||||
getLogger().info("Successfully booted in {}s!", launchTimer);
|
||||
getLogger().info("Initializing shared components...");
|
||||
|
||||
@@ -113,7 +112,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
|
||||
// Start threads
|
||||
getLogger().info("Starting all managed threads...");
|
||||
SonarServiceManager.start();
|
||||
ScheduledServiceManager.start();
|
||||
|
||||
// Done
|
||||
getLogger().info("Done ({}s)!", launchTimer);
|
||||
@@ -121,12 +120,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// An error has occurred
|
||||
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
|
||||
throwable.printStackTrace(System.err);
|
||||
return; // Do not check for updates if the launch failed
|
||||
}
|
||||
|
||||
// Check if a new version has been released if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
|
||||
UpdateChecker.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,15 +139,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getLogger().info("Taking cached snapshots of all packets...");
|
||||
FallbackPreparer.prepare();
|
||||
|
||||
// Update ratelimiter caches
|
||||
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
// Update ratelimiter cache
|
||||
getFallback().setRatelimiter(getConfig().getVerification().getReconnectDelay() > 0L
|
||||
? new CaffeineCacheRatelimiter(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
: NoopCacheRatelimiter.INSTANCE);
|
||||
|
||||
// Update blacklist cache
|
||||
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
|
||||
@@ -180,14 +168,14 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Close the old connection first
|
||||
verifiedPlayerController.close();
|
||||
}
|
||||
verifiedPlayerController = new VerifiedPlayerController();
|
||||
verifiedPlayerController = new VerifiedPlayerController(libraryManager);
|
||||
}
|
||||
|
||||
public final void shutdown() {
|
||||
// Initialize the shutdown process
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// Interrupt threads
|
||||
SonarServiceManager.stop();
|
||||
ScheduledServiceManager.stop();
|
||||
// Close database connection if present
|
||||
if (verifiedPlayerController != null) {
|
||||
verifiedPlayerController.close();
|
||||
+8
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -25,9 +25,7 @@ import io.netty.channel.ChannelPromise;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
|
||||
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -35,27 +33,22 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
public static final ChannelHandler INSTANCE = new FallbackBandwidthHandler();
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
public void channelRead(final ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the incoming traffic by the number of readable bytes
|
||||
INCOMING.increment(((ByteBuf) msg).readableBytes());
|
||||
final int readableBytes = ((ByteBuf) msg).readableBytes();
|
||||
GlobalSonarStatistics.perSecondIncomingTraffic += readableBytes;
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg,
|
||||
final ChannelPromise promise) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the outgoing traffic by the number of readable bytes
|
||||
OUTGOING.increment(((ByteBuf) msg).readableBytes());
|
||||
final int readableBytes = ((ByteBuf) msg).readableBytes();
|
||||
GlobalSonarStatistics.perSecondOutgoingTraffic += readableBytes;
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.write(msg, promise);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Consumer<ChannelPipeline> sonarPipelineInjector;
|
||||
@Setter
|
||||
@Getter
|
||||
private InetAddress inetAddress;
|
||||
|
||||
@Override
|
||||
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Increase connections per second for the action bar verbose
|
||||
GlobalSonarStatistics.countConnection();
|
||||
// Make sure to let the server handle this
|
||||
ctx.fireChannelActive();
|
||||
// Add the packet handler pipeline
|
||||
sonarPipelineInjector.accept(ctx.pipeline());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Make sure to let the server handle this
|
||||
ctx.fireChannelInactive();
|
||||
// The player can disconnect without sending the login packet first
|
||||
// Account for this by checking if the inetAddress has been set yet
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the queue
|
||||
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (ignored, action) -> {
|
||||
// The player is not queued, so we need to remove them from other maps as well
|
||||
if (action == null) {
|
||||
// Remove the IP address from the connected players, if needed
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> {
|
||||
/*
|
||||
* Remove this account from the online players or decrement the number of accounts
|
||||
* with the same IP, but only if the player is logging into the backend server.
|
||||
* We don't need to decrement the count if the player was just verified since
|
||||
* we've never actually incremented it in the first place ¯\_(ツ)_/¯
|
||||
*/
|
||||
if (v == null) {
|
||||
Sonar.get().getFallback().getOnline().compute(inetAddress,
|
||||
(ignored1, count) -> count == null || count <= 1 ? null : count - 1);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We can override the default exceptionCaught method since this handler
|
||||
* will run before the connection knows that there has been an error.
|
||||
*/
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final Throwable cause) throws Exception {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.fingerprint.FingerprintingUtil;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.GeyserUtil;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
|
||||
protected @Nullable String username;
|
||||
protected ProtocolVersion protocolVersion;
|
||||
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
|
||||
|
||||
/**
|
||||
* Validates and handles incoming handshake packets
|
||||
*/
|
||||
protected final void handleHandshake(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull String hostname,
|
||||
final int protocol) throws Exception {
|
||||
// Check if the hostname is invalid
|
||||
if (hostname.isEmpty()) {
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
ctx.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and handles incoming login packets
|
||||
*/
|
||||
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Runnable initialLoginAction,
|
||||
final @NotNull String username,
|
||||
final @NotNull InetSocketAddress socketAddress) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (this.username != null) {
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
// Connections from unknown protocol versions will be discarded
|
||||
// as this is the safest way of handling unwanted connections.
|
||||
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
|
||||
if (protocolVersion.isUnknown()) {
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
GlobalSonarStatistics.countLogin();
|
||||
this.username = username;
|
||||
|
||||
final InetAddress inetAddress = socketAddress.getAddress();
|
||||
ctx.pipeline().get(FallbackInboundHandler.class).setInetAddress(inetAddress);
|
||||
|
||||
// Check if Fallback is already verifying a player with the same IP address
|
||||
if (Sonar.get().getFallback().getConnected().containsKey(inetAddress)) {
|
||||
customDisconnect(ctx.channel(), alreadyVerifying, protocolVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(ctx.channel(), protocolBlacklisted, protocolVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
final String hostAddress = inetAddress.getHostAddress();
|
||||
// Check if the player failed the verification too many times
|
||||
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
if (limit > 0) {
|
||||
final int score = Sonar.get().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
|
||||
if (score >= limit) {
|
||||
customDisconnect(ctx.channel(), blacklisted, protocolVersion);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
initialLogin(ctx.channel(), inetAddress, initialLoginAction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections if configured
|
||||
final boolean geyser = GeyserUtil.isGeyserConnection(ctx.channel(), socketAddress);
|
||||
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
|
||||
initialLogin(ctx.channel(), inetAddress, initialLoginAction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
|
||||
initialLogin(ctx.channel(), inetAddress, initialLoginAction);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!Sonar.get().getFallback().getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(ctx.channel(), reconnectedTooFast, protocolVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all other pipelines that could still mess up something
|
||||
rewriteProtocol(ctx, channelRemovalListener);
|
||||
|
||||
// Queue the connection for further processing
|
||||
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (__, runnable) -> {
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (runnable != null) {
|
||||
customDisconnect(ctx.channel(), alreadyQueued, protocolVersion);
|
||||
// Remove other instances of this IP address from the queue
|
||||
return null;
|
||||
}
|
||||
|
||||
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(username).matches()) {
|
||||
customDisconnect(ctx.channel(), invalidUsername, protocolVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the user and let the verification handler take over the channel
|
||||
new FallbackUserWrapper(ctx.channel(), inetAddress, protocolVersion, username, fingerprint, geyser);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the maximum accounts per IP limit check before letting the player join
|
||||
*/
|
||||
protected final void initialLogin(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull Runnable loginPacket) throws Exception {
|
||||
Sonar.get().getFallback().getOnline().compute(inetAddress, (__, count) -> {
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Skip the maximum online per IP check if it's disabled in the configuration
|
||||
if (count != null && maxOnlinePerIp > 0) {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
if (count >= maxOnlinePerIp) {
|
||||
customDisconnect(channel, tooManyOnlinePerIP, protocolVersion);
|
||||
return count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Let the server know about the login packet
|
||||
loginPacket.run();
|
||||
// Increment the number of accounts with the same IP
|
||||
return count == null ? 1 : count + 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all pipelines and rewrites them using our custom handlers
|
||||
*/
|
||||
private static void rewriteProtocol(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull RemovalListener removalListener) {
|
||||
for (final Map.Entry<String, ChannelHandler> entry : ctx.pipeline()) {
|
||||
// Don't accidentally remove Sonar's handlers
|
||||
if (entry.getKey().startsWith("sonar")
|
||||
// Don't remove floodgate's pipelines
|
||||
|| entry.getKey().startsWith("geyser")
|
||||
|| entry.getKey().startsWith("floodgate")
|
||||
|| entry.getKey().equals("DefaultChannelPipeline$TailContext#0")) {
|
||||
continue;
|
||||
}
|
||||
ctx.pipeline().remove(entry.getValue());
|
||||
removalListener.accept(ctx.pipeline(), entry.getKey(), entry.getValue());
|
||||
}
|
||||
// Add our read/write timeout handler
|
||||
ctx.pipeline().addFirst(FALLBACK_TIMEOUT, new FallbackTimeoutHandler(
|
||||
Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
Sonar.get().getConfig().getVerification().getWriteTimeout(),
|
||||
TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
private static void customDisconnect(final @NotNull Channel channel,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (channel.isActive()) {
|
||||
if (channel.eventLoop().inEventLoop()) {
|
||||
_customDisconnect(channel, packet, protocolVersion);
|
||||
} else {
|
||||
channel.eventLoop().execute(() -> _customDisconnect(channel, packet, protocolVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the player before verification (during login)
|
||||
*/
|
||||
private static void _customDisconnect(final @NotNull Channel channel,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
// Remove the connection handler pipeline to completely take over the channel
|
||||
final String handler = Sonar.get().getPlatform().getConnectionHandler();
|
||||
if (channel.pipeline().context(handler) != null) {
|
||||
channel.pipeline().remove(handler);
|
||||
}
|
||||
final String encoder = Sonar.get().getPlatform().getEncoder().apply(channel.pipeline());
|
||||
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
|
||||
// Close the channel if no encoder exists
|
||||
if (currentEncoder != null) {
|
||||
// We don't need to update the encoder if it has already been replaced by Sonar
|
||||
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
}
|
||||
ProtocolUtil.closeWith(channel, protocolVersion, packet);
|
||||
} else {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RemovalListener {
|
||||
void accept(final @NotNull ChannelPipeline pipeline,
|
||||
final @NotNull String name,
|
||||
final @NotNull ChannelHandler handler);
|
||||
|
||||
RemovalListener EMPTY = (pipeline, name, handler) -> {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarIntLengthEncoder;
|
||||
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.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.verification.FallbackPreJoinHandler;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
|
||||
public final class FallbackUserWrapper implements FallbackUser {
|
||||
@Accessors(fluent = true)
|
||||
private final Channel channel;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final String fingerprint;
|
||||
private final String username;
|
||||
private final boolean geyser;
|
||||
private final SystemTimer loginTimer;
|
||||
@Setter
|
||||
private boolean forceCaptcha;
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull String username,
|
||||
final @NotNull String fingerprint,
|
||||
final boolean geyser) {
|
||||
this.channel = channel;
|
||||
this.inetAddress = inetAddress;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.username = username;
|
||||
this.fingerprint = fingerprint;
|
||||
this.geyser = geyser;
|
||||
this.loginTimer = new SystemTimer();
|
||||
|
||||
GlobalSonarStatistics.totalAttemptedVerifications++;
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()
|
||||
&& (Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack())) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
|
||||
.replace("<username>", username)
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(this));
|
||||
|
||||
// Run this in the channel's event loop to avoid issues
|
||||
channel.eventLoop().execute(() -> {
|
||||
// Make sure the channel is still active
|
||||
if (!channel.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the player as connected by caching them in a map of verifying players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> true);
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
// TODO: recode injection
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
channel.pipeline().addFirst(FALLBACK_FRAME_ENCODER, FallbackVarIntLengthEncoder.INSTANCE);
|
||||
channel.pipeline().addLast(FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
// TODO: recode injection
|
||||
final FallbackPacketDecoder newDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
channel.pipeline().addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
|
||||
channel.pipeline().addLast(FALLBACK_PACKET_DECODER, newDecoder);
|
||||
|
||||
// We're sending the LoginSuccess packet now
|
||||
newDecoder.updateRegistry(FallbackPacketRegistry.LOGIN);
|
||||
newEncoder.updateRegistry(FallbackPacketRegistry.LOGIN);
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(FallbackPreparer.loginSuccess);
|
||||
|
||||
// pre-1.20.2 clients do not have the configuration stage
|
||||
if (protocolVersion.lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||
newDecoder.updateRegistry(FallbackPacketRegistry.GAME);
|
||||
newEncoder.updateRegistry(FallbackPacketRegistry.GAME);
|
||||
}
|
||||
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
newDecoder.setListener(new FallbackPreJoinHandler(this));
|
||||
|
||||
// Make sure to catch all exceptions during the verification
|
||||
// TODO: recode injection
|
||||
channel.pipeline().addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
final FallbackPacketEncoder encoder = channel.pipeline().get(FallbackPacketEncoder.class);
|
||||
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() == FallbackPacketRegistry.LOGIN;
|
||||
ProtocolUtil.closeWith(channel, protocolVersion, new DisconnectPacket(reason, duringLogin));
|
||||
}
|
||||
}
|
||||
+23
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,19 +15,26 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.injection;
|
||||
package xyz.jonesdev.sonar.common.fallback.netty;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
|
||||
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
|
||||
import xyz.jonesdev.sonar.common.util.exception.ReflectiveOperationException;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackInjectedChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private static final MethodHandle INIT_CHANNEL_METHOD;
|
||||
@@ -41,8 +48,8 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
|
||||
}
|
||||
}
|
||||
|
||||
private final ChannelInitializer<Channel> originalChannelInitializer;
|
||||
private final Consumer<ChannelPipeline> sonarPipelineInjector;
|
||||
private final @NotNull ChannelInitializer<Channel> originalChannelInitializer;
|
||||
private final @NotNull Consumer<ChannelPipeline> sonarPipelineInjector;
|
||||
|
||||
@Override
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
@@ -53,9 +60,17 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
// Inject Sonar's channel handler into the pipeline
|
||||
if (channel.isActive()) {
|
||||
sonarPipelineInjector.accept(channel.pipeline());
|
||||
// Inject Sonar's channel handler into the pipeline;
|
||||
// Also make sure the player is not a fake player to avoid compatibility issues
|
||||
if (channel.isActive() && !FakeChannelUtil.isFakeChannel(channel)) {
|
||||
final FallbackInboundHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
|
||||
// We need to be careful on Bukkit, as the encoder can be different
|
||||
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
|
||||
final String encoder = Sonar.get().getPlatform().getEncoder().apply(channel.pipeline());
|
||||
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
|
||||
} else {
|
||||
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.netty;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public final class FallbackTailExceptionsHandler extends ChannelDuplexHandler {
|
||||
public static final FallbackTailExceptionsHandler INSTANCE = new FallbackTailExceptionsHandler();
|
||||
|
||||
// We can override the default exceptionCaught method since the server
|
||||
// does not have any other pipelines that could handle this error.
|
||||
// Additionally, this will also run after our custom decoder.
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final Throwable cause) throws Exception {
|
||||
// Close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
package xyz.jonesdev.sonar.common.fallback.netty;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
@@ -26,12 +26,12 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
public FallbackTimeoutHandler(final int readTimeout, final int writeTimeout, final TimeUnit timeUnit) {
|
||||
super(readTimeout, writeTimeout, 0L, timeUnit);
|
||||
super(false, readTimeout, writeTimeout, 0L, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
protected void channelIdle(final @NotNull ChannelHandlerContext ctx,
|
||||
final IdleStateEvent idleStateEvent) throws Exception {
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
// still throws the Exception even if the channel is closed
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.netty;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.util.ByteProcessor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
|
||||
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive()) {
|
||||
byteBuf.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip any runs of 0x00 we might find
|
||||
final int packetStart = byteBuf.forEachByte(ByteProcessor.FIND_NON_NUL);
|
||||
if (packetStart == -1) {
|
||||
return;
|
||||
}
|
||||
byteBuf.readerIndex(packetStart);
|
||||
|
||||
// Try to read the length of the packet
|
||||
byteBuf.markReaderIndex();
|
||||
final int preIndex = byteBuf.readerIndex();
|
||||
final int length = readRawVarInt21(byteBuf);
|
||||
if (preIndex == byteBuf.readerIndex()) {
|
||||
return;
|
||||
}
|
||||
if (length <= 0) {
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Bad VarInt length: " + length) : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
if (byteBuf.readableBytes() < length) {
|
||||
byteBuf.resetReaderIndex();
|
||||
} else {
|
||||
out.add(byteBuf.readRetainedSlice(length));
|
||||
}
|
||||
}
|
||||
|
||||
private static int readRawVarInt21(final @NotNull ByteBuf byteBuf) {
|
||||
if (byteBuf.readableBytes() < 4) {
|
||||
return readRawVarIntSmallBuffer(byteBuf);
|
||||
}
|
||||
|
||||
// take the last three bytes and check if any of them have the high bit set
|
||||
final int wholeOrMore = byteBuf.getIntLE(byteBuf.readerIndex());
|
||||
final int atStop = ~wholeOrMore & 0x808080;
|
||||
if (atStop == 0) {
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("VarInt too big") : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
final int bitsToKeep = Integer.numberOfTrailingZeros(atStop) + 1;
|
||||
byteBuf.skipBytes(bitsToKeep >> 3);
|
||||
|
||||
// https://github.com/netty/netty/pull/14050#issuecomment-2107750734
|
||||
int preservedBytes = wholeOrMore & (atStop ^ (atStop - 1));
|
||||
|
||||
// https://github.com/netty/netty/pull/14050#discussion_r1597896639
|
||||
preservedBytes = (preservedBytes & 0x007F007F) | ((preservedBytes & 0x00007F00) >> 1);
|
||||
preservedBytes = (preservedBytes & 0x00003FFF) | ((preservedBytes & 0x3FFF0000) >> 2);
|
||||
return preservedBytes;
|
||||
}
|
||||
|
||||
private static int readRawVarIntSmallBuffer(final @NotNull ByteBuf byteBuf) {
|
||||
if (!byteBuf.isReadable()) {
|
||||
return 0;
|
||||
}
|
||||
byteBuf.markReaderIndex();
|
||||
|
||||
byte tmp = byteBuf.readByte();
|
||||
if (tmp >= 0) {
|
||||
return tmp;
|
||||
}
|
||||
int result = tmp & 0x7F;
|
||||
if (!byteBuf.isReadable()) {
|
||||
byteBuf.resetReaderIndex();
|
||||
return 0;
|
||||
}
|
||||
if ((tmp = byteBuf.readByte()) >= 0) {
|
||||
return result | tmp << 7;
|
||||
}
|
||||
result |= (tmp & 0x7F) << 7;
|
||||
if (!byteBuf.isReadable()) {
|
||||
byteBuf.resetReaderIndex();
|
||||
return 0;
|
||||
}
|
||||
if ((tmp = byteBuf.readByte()) >= 0) {
|
||||
return result | tmp << 14;
|
||||
}
|
||||
return result | (tmp & 0x7F) << 14;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.netty;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
|
||||
@ChannelHandler.Sharable
|
||||
public final class FallbackVarIntLengthEncoder extends MessageToMessageEncoder<ByteBuf> {
|
||||
public static final FallbackVarIntLengthEncoder INSTANCE = new FallbackVarIntLengthEncoder();
|
||||
|
||||
@Override
|
||||
protected void encode(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull List<Object> out) throws Exception {
|
||||
final int readableBytes = byteBuf.readableBytes();
|
||||
final int length = ProtocolUtil.varIntBytes(readableBytes);
|
||||
|
||||
final ByteBuf lenBuf = ctx.alloc().buffer(length);
|
||||
|
||||
ProtocolUtil.writeVarInt(lenBuf, readableBytes);
|
||||
out.add(lenBuf);
|
||||
out.add(byteBuf.retain());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.CaptchaGenerationEndEvent;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapCaptchaInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapColorPalette;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@UtilityClass
|
||||
public class CaptchaPreparer {
|
||||
private final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
private final Random RANDOM = new Random();
|
||||
|
||||
private MapCaptchaInfo[] cached;
|
||||
private int preparedAmount;
|
||||
|
||||
public void prepare() {
|
||||
// Make sure we're not preparing when Sonar is already preparing answers
|
||||
if (cached != null && preparedAmount != cached.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
|
||||
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
|
||||
|
||||
// Prepare cache
|
||||
final int precomputeAmount = Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount();
|
||||
final char[] alphabet = Sonar.get().getConfig().getVerification().getMap().getAlphabet().toCharArray();
|
||||
|
||||
preparedAmount = 0;
|
||||
cached = new MapCaptchaInfo[precomputeAmount];
|
||||
|
||||
// Prepare everything asynchronously
|
||||
PREPARATION_SERVICE.execute(() -> {
|
||||
for (preparedAmount = 0; preparedAmount < precomputeAmount;) {
|
||||
// Generate CAPTCHA
|
||||
final char[] answer = new char[3 + RANDOM.nextInt(2)];
|
||||
for (int j = 0; j < answer.length; j++) {
|
||||
answer[j] = alphabet[RANDOM.nextInt(alphabet.length)];
|
||||
}
|
||||
final BufferedImage image = Sonar.get().getFallback().getCaptchaGenerator().createImage(answer);
|
||||
// Convert and cache converted Minecraft map bytes
|
||||
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), MapColorPalette.imageToBuffer(image));
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
|
||||
Sonar.get().getEventManager().publish(new CaptchaGenerationEndEvent(timer, preparedAmount));
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isCaptchaAvailable() {
|
||||
return preparedAmount > 0 && cached != null && cached[0] != null;
|
||||
}
|
||||
|
||||
public MapCaptchaInfo getRandomCaptcha() {
|
||||
// Give the player a random CAPTCHA out of the ones that we've already prepared
|
||||
return cached[RANDOM.nextInt(preparedAmount)];
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -39,20 +39,18 @@ public interface FallbackPacket {
|
||||
void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception;
|
||||
|
||||
/**
|
||||
* @param byteBuf ByteBuf
|
||||
* @param protocolVersion Protocol version of the player
|
||||
* @return The minimum allowed length of the decoded packet
|
||||
*/
|
||||
default int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
default int expectedMinLength(final ProtocolVersion protocolVersion) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param byteBuf ByteBuf
|
||||
* @param protocolVersion Protocol version of the player
|
||||
* @return The maximum allowed length of the decoded packet
|
||||
*/
|
||||
default int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
default int expectedMaxLength(final ProtocolVersion protocolVersion) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
+31
-31
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -20,35 +20,30 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.maxTotalPacketsSent;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
@Setter
|
||||
private FallbackPacketListener listener;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
|
||||
}
|
||||
private int totalPacketsSent;
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
|
||||
this.registry = registry.getProtocolRegistry(FallbackPacketRegistry.Direction.SERVERBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg) throws Exception {
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
final ByteBuf byteBuf = (ByteBuf) msg;
|
||||
|
||||
@@ -60,8 +55,13 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow the player to spam packets to overload netty
|
||||
if (++totalPacketsSent > maxTotalPacketsSent) {
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Sent too many packets") : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
// Read the packet ID and then create the packet from it
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final int packetId = ProtocolUtil.readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// Skip the packet if it's not registered within Sonar's packet registry
|
||||
@@ -70,27 +70,27 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
// Ensure that the packet isn't too large or too small
|
||||
checkPacketSize(byteBuf, packet);
|
||||
checkPacketSize(byteBuf.readableBytes(), packet);
|
||||
|
||||
try {
|
||||
// Try to decode the packet for the given protocol version
|
||||
packet.decode(byteBuf, protocolVersion);
|
||||
} catch (Throwable throwable) {
|
||||
throw new CorruptedFrameException("Failed to decode packet");
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
// Check if the packet still has bytes left after we decoded it
|
||||
if (byteBuf.isReadable()) {
|
||||
throw new CorruptedFrameException("Could not read packet to end");
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Could not read packet to end ("
|
||||
+ byteBuf.readableBytes() + " bytes left)"): QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
// Let our verification handler process the packet
|
||||
if (listener != null) {
|
||||
listener.handle(packet);
|
||||
// Make sure to let the timeout handler know about this packet
|
||||
ctx.fireChannelRead(packet);
|
||||
}
|
||||
|
||||
// Fire channel read to avoid timeout
|
||||
ctx.fireChannelRead(packet);
|
||||
} finally {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
@@ -98,16 +98,16 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
|
||||
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
|
||||
throw new CorruptedFrameException("Packet too large");
|
||||
private void checkPacketSize(final int packetSize, final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int expectedMaxLen = packet.expectedMaxLength(protocolVersion);
|
||||
if (expectedMaxLen != -1 && packetSize > expectedMaxLen) {
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Packet too large: " + packetSize + " max: " + expectedMaxLen)
|
||||
: QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
|
||||
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
|
||||
throw new CorruptedFrameException("Packet too small");
|
||||
final int expectedMinLen = packet.expectedMinLength(protocolVersion);
|
||||
if (expectedMinLen != -1 && packetSize < expectedMinLen) {
|
||||
throw ProtocolUtil.DEBUG ? new DecoderException("Packet too small: " + packetSize + " min: " + expectedMinLen)
|
||||
: QuietDecoderException.INSTANCE;
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -21,37 +21,31 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
|
||||
private final ProtocolVersion protocolVersion;
|
||||
@Getter
|
||||
private FallbackPacketRegistry packetRegistry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry protocolRegistry;
|
||||
|
||||
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
updateRegistry(LOGIN);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry packetRegistry) {
|
||||
this.packetRegistry = packetRegistry;
|
||||
this.protocolRegistry = packetRegistry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
|
||||
this.protocolRegistry = packetRegistry.getProtocolRegistry(FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final ByteBuf out) throws Exception {
|
||||
final FallbackPacket originalPacket = packet instanceof FallbackPacketSnapshot
|
||||
? ((FallbackPacketSnapshot) packet).getOriginalPacket() : packet;
|
||||
final @NotNull ByteBuf out) throws Exception {
|
||||
final Class<? extends FallbackPacket> originalPacket = packet instanceof FallbackPacketSnapshot
|
||||
? ((FallbackPacketSnapshot) packet).getOriginalPacketClass() : packet.getClass();
|
||||
final int packetId = protocolRegistry.getPacketId(originalPacket);
|
||||
writeVarInt(out, packetId);
|
||||
ProtocolUtil.writeVarInt(out, packetId);
|
||||
packet.encode(out, protocolVersion);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
+274
-156
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -20,24 +20,34 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
import io.netty.util.collection.IntObjectHashMap;
|
||||
import io.netty.util.collection.IntObjectMap;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
|
||||
public enum FallbackPacketRegistry {
|
||||
HANDSHAKE {
|
||||
{
|
||||
serverbound.register(HandshakePacket.class, HandshakePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false));
|
||||
}
|
||||
},
|
||||
LOGIN {
|
||||
{
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
@@ -45,6 +55,8 @@ public enum FallbackPacketRegistry {
|
||||
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
|
||||
map(0x02, MINECRAFT_1_7_2, true));
|
||||
|
||||
serverbound.register(LoginStartPacket.class, LoginStartPacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false));
|
||||
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
}
|
||||
@@ -55,11 +67,11 @@ public enum FallbackPacketRegistry {
|
||||
map(0x01, MINECRAFT_1_20_2, true),
|
||||
map(0x02, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false),
|
||||
map(0x03, MINECRAFT_1_20_5, false));
|
||||
map(0x02, MINECRAFT_1_20_2, true),
|
||||
map(0x03, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false),
|
||||
map(0x04, MINECRAFT_1_20_5, false));
|
||||
map(0x03, MINECRAFT_1_20_2, true),
|
||||
map(0x04, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
|
||||
map(0x05, MINECRAFT_1_20_2, true),
|
||||
map(0x07, MINECRAFT_1_20_5, true));
|
||||
@@ -93,22 +105,24 @@ public enum FallbackPacketRegistry {
|
||||
map(0x24, MINECRAFT_1_19_3, true),
|
||||
map(0x28, MINECRAFT_1_19_4, true),
|
||||
map(0x29, MINECRAFT_1_20_2, true),
|
||||
map(0x2B, MINECRAFT_1_20_5, true));
|
||||
map(0x2B, MINECRAFT_1_20_5, true),
|
||||
map(0x2C, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x1F, MINECRAFT_1_9, false),
|
||||
map(0x21, MINECRAFT_1_13, false),
|
||||
map(0x20, MINECRAFT_1_14, false),
|
||||
map(0x21, MINECRAFT_1_15, false),
|
||||
map(0x20, MINECRAFT_1_16, false),
|
||||
map(0x1F, MINECRAFT_1_16_2, false),
|
||||
map(0x21, MINECRAFT_1_17, false),
|
||||
map(0x1E, MINECRAFT_1_19, false),
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x23, MINECRAFT_1_19_4, false),
|
||||
map(0x24, MINECRAFT_1_20_2, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
map(0x00, MINECRAFT_1_7_2, true),
|
||||
map(0x1F, MINECRAFT_1_9, true),
|
||||
map(0x21, MINECRAFT_1_13, true),
|
||||
map(0x20, MINECRAFT_1_14, true),
|
||||
map(0x21, MINECRAFT_1_15, true),
|
||||
map(0x20, MINECRAFT_1_16, true),
|
||||
map(0x1F, MINECRAFT_1_16_2, true),
|
||||
map(0x21, MINECRAFT_1_17, true),
|
||||
map(0x1E, MINECRAFT_1_19, true),
|
||||
map(0x20, MINECRAFT_1_19_1, true),
|
||||
map(0x1F, MINECRAFT_1_19_3, true),
|
||||
map(0x23, MINECRAFT_1_19_4, true),
|
||||
map(0x24, MINECRAFT_1_20_2, true),
|
||||
map(0x26, MINECRAFT_1_20_5, true),
|
||||
map(0x27, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
map(0x40, MINECRAFT_1_7_2, true),
|
||||
map(0x1A, MINECRAFT_1_9, true),
|
||||
@@ -124,7 +138,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1A, MINECRAFT_1_19_4, true),
|
||||
map(0x1B, MINECRAFT_1_20_2, true),
|
||||
map(0x1D, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
|
||||
clientbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
|
||||
map(0x08, MINECRAFT_1_7_2, true),
|
||||
map(0x2E, MINECRAFT_1_9, true),
|
||||
map(0x2F, MINECRAFT_1_12_1, true),
|
||||
@@ -139,7 +153,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x38, MINECRAFT_1_19_3, true),
|
||||
map(0x3C, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_20_2, true),
|
||||
map(0x40, MINECRAFT_1_20_5, true));
|
||||
map(0x40, MINECRAFT_1_20_5, true),
|
||||
map(0x42, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(PlayerAbilitiesPacket.class, PlayerAbilitiesPacket::new,
|
||||
map(0x39, MINECRAFT_1_7_2, true),
|
||||
map(0x2B, MINECRAFT_1_9, true),
|
||||
@@ -155,7 +170,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x30, MINECRAFT_1_19_3, true),
|
||||
map(0x34, MINECRAFT_1_19_4, true),
|
||||
map(0x36, MINECRAFT_1_20_2, true),
|
||||
map(0x38, MINECRAFT_1_20_5, true));
|
||||
map(0x38, MINECRAFT_1_20_5, true),
|
||||
map(0x3A, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(ChunkDataPacket.class, ChunkDataPacket::new,
|
||||
map(0x21, MINECRAFT_1_7_2, true),
|
||||
map(0x20, MINECRAFT_1_9, true),
|
||||
@@ -170,7 +186,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_3, true),
|
||||
map(0x24, MINECRAFT_1_19_4, true),
|
||||
map(0x25, MINECRAFT_1_20_2, true),
|
||||
map(0x27, MINECRAFT_1_20_5, true));
|
||||
map(0x27, MINECRAFT_1_20_5, true),
|
||||
map(0x28, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(RemoveEntitiesPacket.class, RemoveEntitiesPacket::new,
|
||||
map(0x13, MINECRAFT_1_7_2, true),
|
||||
map(0x30, MINECRAFT_1_9, true),
|
||||
@@ -188,7 +205,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x3E, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_19_4, true),
|
||||
map(0x40, MINECRAFT_1_20_2, true),
|
||||
map(0x42, MINECRAFT_1_20_5, true));
|
||||
map(0x42, MINECRAFT_1_20_5, true),
|
||||
map(0x47, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(SetPassengersPacket.class, SetPassengersPacket::new,
|
||||
map(0x1B, MINECRAFT_1_7_2, true),
|
||||
map(0x40, MINECRAFT_1_9, true),
|
||||
@@ -203,27 +221,27 @@ public enum FallbackPacketRegistry {
|
||||
map(0x59, MINECRAFT_1_19_4, true),
|
||||
map(0x5B, MINECRAFT_1_20_2, true),
|
||||
map(0x5D, MINECRAFT_1_20_3, true),
|
||||
map(0x5F, MINECRAFT_1_20_5, true));
|
||||
map(0x5F, MINECRAFT_1_20_5, true),
|
||||
map(0x65, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
|
||||
map(0x29, MINECRAFT_1_9, true),
|
||||
map(0x2B, MINECRAFT_1_13, true),
|
||||
map(0x2C, MINECRAFT_1_14, true),
|
||||
map(0x2D, MINECRAFT_1_15, true),
|
||||
map(0x2C, MINECRAFT_1_16, true),
|
||||
map(0x2B, MINECRAFT_1_16_2, true),
|
||||
map(0x2C, MINECRAFT_1_17, true),
|
||||
map(0x29, MINECRAFT_1_19, true),
|
||||
map(0x2B, MINECRAFT_1_19_1, true),
|
||||
map(0x2A, MINECRAFT_1_19_3, true),
|
||||
map(0x2E, MINECRAFT_1_19_4, true),
|
||||
map(0x2F, MINECRAFT_1_20_2, true),
|
||||
map(0x31, MINECRAFT_1_20_5, true),
|
||||
map(0x33, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
|
||||
map(0x0E, MINECRAFT_1_7_2, true),
|
||||
map(0x00, MINECRAFT_1_9, true),
|
||||
map(0x01, MINECRAFT_1_19_4, true));
|
||||
clientbound.register(PlayerInfoPacket.class, PlayerInfoPacket::new,
|
||||
map(0x38, MINECRAFT_1_7_2, true),
|
||||
map(0x2D, MINECRAFT_1_9, true),
|
||||
map(0x2E, MINECRAFT_1_12_1, true),
|
||||
map(0x30, MINECRAFT_1_13, true),
|
||||
map(0x33, MINECRAFT_1_14, true),
|
||||
map(0x34, MINECRAFT_1_15, true),
|
||||
map(0x33, MINECRAFT_1_16, true),
|
||||
map(0x32, MINECRAFT_1_16_2, true),
|
||||
map(0x36, MINECRAFT_1_17, true),
|
||||
map(0x34, MINECRAFT_1_19, true),
|
||||
map(0x37, MINECRAFT_1_19_1, true),
|
||||
map(0x36, MINECRAFT_1_19_3, true),
|
||||
map(0x3A, MINECRAFT_1_19_4, true),
|
||||
map(0x3C, MINECRAFT_1_20_2, true),
|
||||
map(0x3E, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
|
||||
map(0x22, MINECRAFT_1_7_2, true),
|
||||
map(0x10, MINECRAFT_1_9, true),
|
||||
@@ -238,7 +256,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x43, MINECRAFT_1_19_4, true),
|
||||
map(0x45, MINECRAFT_1_20_2, true),
|
||||
map(0x47, MINECRAFT_1_20_3, true),
|
||||
map(0x49, MINECRAFT_1_20_5, true));
|
||||
map(0x49, MINECRAFT_1_20_5, true),
|
||||
map(0x4E, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(TransactionPacket.class, TransactionPacket::new,
|
||||
map(0x32, MINECRAFT_1_7_2, true),
|
||||
map(0x11, MINECRAFT_1_9, true),
|
||||
@@ -252,7 +271,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2E, MINECRAFT_1_19_3, true),
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true),
|
||||
map(0x35, MINECRAFT_1_20_5, true));
|
||||
map(0x35, MINECRAFT_1_20_5, true),
|
||||
map(0x37, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(SetDefaultSpawnPositionPacket.class, SetDefaultSpawnPositionPacket::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
@@ -269,7 +289,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x50, MINECRAFT_1_19_4, true),
|
||||
map(0x52, MINECRAFT_1_20_2, true),
|
||||
map(0x54, MINECRAFT_1_20_3, true),
|
||||
map(0x56, MINECRAFT_1_20_5, true));
|
||||
map(0x56, MINECRAFT_1_20_5, true),
|
||||
map(0x5B, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(MapDataPacket.class, MapDataPacket::new,
|
||||
map(0x34, MINECRAFT_1_7_2, true),
|
||||
map(0x24, MINECRAFT_1_9, true),
|
||||
@@ -283,7 +304,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x25, MINECRAFT_1_19_3, true),
|
||||
map(0x29, MINECRAFT_1_19_4, true),
|
||||
map(0x2A, MINECRAFT_1_20_2, true),
|
||||
map(0x2C, MINECRAFT_1_20_5, true));
|
||||
map(0x2C, MINECRAFT_1_20_5, true),
|
||||
map(0x2D, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(SetContainerSlotPacket.class, SetContainerSlotPacket::new,
|
||||
map(0x2F, MINECRAFT_1_7_2, true),
|
||||
map(0x16, MINECRAFT_1_9, true),
|
||||
@@ -311,26 +333,30 @@ public enum FallbackPacketRegistry {
|
||||
map(0x56, MINECRAFT_1_19_4, true),
|
||||
map(0x58, MINECRAFT_1_20_2, true),
|
||||
map(0x5A, MINECRAFT_1_20_3, true),
|
||||
map(0x5C, MINECRAFT_1_20_5, true));
|
||||
map(0x5C, MINECRAFT_1_20_5, true),
|
||||
map(0x61, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(SystemChatPacket.class, SystemChatPacket::new,
|
||||
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_3, false),
|
||||
map(0x6C, MINECRAFT_1_20_5, false));
|
||||
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_17, 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(0x69, MINECRAFT_1_20_3, true),
|
||||
map(0x6C, MINECRAFT_1_20_5, true),
|
||||
map(0x73, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(GameEventPacket.class, GameEventPacket::new,
|
||||
map(0x20, MINECRAFT_1_20_3, true),
|
||||
map(0x22, MINECRAFT_1_20_5, true));
|
||||
map(0x22, MINECRAFT_1_20_5, true),
|
||||
map(0x23, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(TransferPacket.class, TransferPacket::new,
|
||||
map(0x73, MINECRAFT_1_20_5, true));
|
||||
map(0x73, MINECRAFT_1_20_5, true),
|
||||
map(0x7A, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, true),
|
||||
map(0x37, MINECRAFT_1_9, true),
|
||||
@@ -347,7 +373,54 @@ public enum FallbackPacketRegistry {
|
||||
map(0x4D, MINECRAFT_1_19_4, true),
|
||||
map(0x4F, MINECRAFT_1_20_2, true),
|
||||
map(0x51, MINECRAFT_1_20_3, true),
|
||||
map(0x53, MINECRAFT_1_20_5, true));
|
||||
map(0x53, MINECRAFT_1_20_5, true),
|
||||
map(0x63, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(EntityAnimationPacket.class, EntityAnimationPacket::new,
|
||||
map(0x0B, MINECRAFT_1_7_2, true),
|
||||
map(0x06, MINECRAFT_1_9, true),
|
||||
map(0x05, MINECRAFT_1_16, true),
|
||||
map(0x06, MINECRAFT_1_17, true),
|
||||
map(0x03, MINECRAFT_1_19, true),
|
||||
map(0x04, MINECRAFT_1_19_4, true),
|
||||
map(0x03, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(TeleportEntityPacket.class, TeleportEntityPacket::new,
|
||||
map(0x18, MINECRAFT_1_7_2, true),
|
||||
map(0x4A, MINECRAFT_1_9, true),
|
||||
map(0x49, MINECRAFT_1_9_4, true),
|
||||
map(0x4B, MINECRAFT_1_12, true),
|
||||
map(0x4C, MINECRAFT_1_12_1, true),
|
||||
map(0x50, MINECRAFT_1_13, true),
|
||||
map(0x56, MINECRAFT_1_14, true),
|
||||
map(0x57, MINECRAFT_1_15, true),
|
||||
map(0x56, MINECRAFT_1_16, true),
|
||||
map(0x61, MINECRAFT_1_17, true),
|
||||
map(0x62, MINECRAFT_1_18, true),
|
||||
map(0x63, MINECRAFT_1_19, true),
|
||||
map(0x66, MINECRAFT_1_19_1, true),
|
||||
map(0x64, MINECRAFT_1_19_3, true),
|
||||
map(0x68, MINECRAFT_1_19_4, true),
|
||||
map(0x6B, MINECRAFT_1_20_2, true),
|
||||
map(0x6D, MINECRAFT_1_20_3, true),
|
||||
map(0x70, MINECRAFT_1_20_5, true),
|
||||
map(0x77, MINECRAFT_1_21_2, true));
|
||||
clientbound.register(UpdateTimePacket.class, UpdateTimePacket::new,
|
||||
map(0x03, MINECRAFT_1_7_2, true),
|
||||
map(0x44, MINECRAFT_1_9, true),
|
||||
map(0x46, MINECRAFT_1_12, true),
|
||||
map(0x47, MINECRAFT_1_12_1, true),
|
||||
map(0x4A, MINECRAFT_1_13, true),
|
||||
map(0x4E, MINECRAFT_1_14, true),
|
||||
map(0x4F, MINECRAFT_1_15, true),
|
||||
map(0x4E, MINECRAFT_1_16, true),
|
||||
map(0x58, MINECRAFT_1_17, true),
|
||||
map(0x59, MINECRAFT_1_18, true),
|
||||
map(0x5C, MINECRAFT_1_19_1, true),
|
||||
map(0x5A, MINECRAFT_1_19_3, true),
|
||||
map(0x5E, MINECRAFT_1_19_4, true),
|
||||
map(0x60, MINECRAFT_1_20_2, true),
|
||||
map(0x62, MINECRAFT_1_20_3, true),
|
||||
map(0x64, MINECRAFT_1_20_5, true),
|
||||
map(0x6B, MINECRAFT_1_21_2, true));
|
||||
|
||||
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, false),
|
||||
@@ -361,7 +434,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x28, MINECRAFT_1_19_1, false),
|
||||
map(0x2B, MINECRAFT_1_20_2, false),
|
||||
map(0x2C, MINECRAFT_1_20_3, false),
|
||||
map(0x2F, MINECRAFT_1_20_5, false));
|
||||
map(0x2F, MINECRAFT_1_20_5, false),
|
||||
map(0x31, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(PaddleBoatPacket.class, PaddleBoatPacket::new,
|
||||
map(0x11, MINECRAFT_1_9, false),
|
||||
map(0x12, MINECRAFT_1_12, false),
|
||||
@@ -376,7 +450,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x19, MINECRAFT_1_19_4, false),
|
||||
map(0x1B, MINECRAFT_1_20_2, false),
|
||||
map(0x1C, MINECRAFT_1_20_3, false),
|
||||
map(0x1F, MINECRAFT_1_20_5, false));
|
||||
map(0x1F, MINECRAFT_1_20_5, false),
|
||||
map(0x21, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(PlayerInputPacket.class, PlayerInputPacket::new,
|
||||
map(0x0C, MINECRAFT_1_7_2, false),
|
||||
map(0x15, MINECRAFT_1_9, false),
|
||||
@@ -391,7 +466,24 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1F, MINECRAFT_1_19_4, false),
|
||||
map(0x22, MINECRAFT_1_20_2, false),
|
||||
map(0x23, MINECRAFT_1_20_3, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
map(0x26, MINECRAFT_1_20_5, false),
|
||||
map(0x28, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
|
||||
map(0x10, MINECRAFT_1_9, false),
|
||||
map(0x11, MINECRAFT_1_12, false),
|
||||
map(0x10, MINECRAFT_1_12_1, false),
|
||||
map(0x13, MINECRAFT_1_13, false),
|
||||
map(0x15, MINECRAFT_1_14, false),
|
||||
map(0x16, MINECRAFT_1_16, false),
|
||||
map(0x15, MINECRAFT_1_17, false),
|
||||
map(0x17, MINECRAFT_1_19, false),
|
||||
map(0x18, MINECRAFT_1_19_1, false),
|
||||
map(0x17, MINECRAFT_1_19_3, false),
|
||||
map(0x18, MINECRAFT_1_19_4, false),
|
||||
map(0x1A, MINECRAFT_1_20_2, false),
|
||||
map(0x1B, MINECRAFT_1_20_3, false),
|
||||
map(0x1E, MINECRAFT_1_20_5, false),
|
||||
map(0x20, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(SystemChatPacket.class, SystemChatPacket::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
@@ -401,7 +493,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x04, MINECRAFT_1_19, false),
|
||||
map(0x05, MINECRAFT_1_19_1, false),
|
||||
map(0x05, MINECRAFT_1_19_3, false),
|
||||
map(0x06, MINECRAFT_1_20_5, false));
|
||||
map(0x06, MINECRAFT_1_20_5, false),
|
||||
map(0x07, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x0B, MINECRAFT_1_9, false),
|
||||
@@ -417,7 +510,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x12, MINECRAFT_1_19_4, false),
|
||||
map(0x14, MINECRAFT_1_20_2, false),
|
||||
map(0x15, MINECRAFT_1_20_3, false),
|
||||
map(0x18, MINECRAFT_1_20_5, false));
|
||||
map(0x18, MINECRAFT_1_20_5, false),
|
||||
map(0x1A, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(ClientInformationPacket.class, ClientInformationPacket::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
@@ -429,7 +523,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x07, MINECRAFT_1_19_3, false),
|
||||
map(0x08, MINECRAFT_1_19_4, false),
|
||||
map(0x09, MINECRAFT_1_20_2, false),
|
||||
map(0x0A, MINECRAFT_1_20_5, false));
|
||||
map(0x0A, MINECRAFT_1_20_5, false),
|
||||
map(0xC, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
|
||||
map(0x17, MINECRAFT_1_7_2, false),
|
||||
map(0x09, MINECRAFT_1_9, false),
|
||||
@@ -443,41 +538,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x0C, MINECRAFT_1_19_3, false),
|
||||
map(0x0D, MINECRAFT_1_19_4, false),
|
||||
map(0x0F, MINECRAFT_1_20_2, false),
|
||||
map(0x12, MINECRAFT_1_21, false));
|
||||
serverbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
|
||||
map(0x06, MINECRAFT_1_7_2, false),
|
||||
map(0x0D, MINECRAFT_1_9, false),
|
||||
map(0x0F, MINECRAFT_1_12, false),
|
||||
map(0x0E, MINECRAFT_1_12_1, false),
|
||||
map(0x11, MINECRAFT_1_13, false),
|
||||
map(0x12, MINECRAFT_1_14, false),
|
||||
map(0x13, MINECRAFT_1_16, false),
|
||||
map(0x12, MINECRAFT_1_17, false),
|
||||
map(0x14, MINECRAFT_1_19, false),
|
||||
map(0x15, MINECRAFT_1_19_1, false),
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false),
|
||||
map(0x1B, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(ConfirmTeleportationPacket.class, ConfirmTeleportationPacket::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(SetPlayerPositionPacket.class, SetPlayerPositionPacket::new,
|
||||
map(0x04, MINECRAFT_1_7_2, false),
|
||||
map(0x0C, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_12, false),
|
||||
map(0x0D, MINECRAFT_1_12_1, false),
|
||||
map(0x10, MINECRAFT_1_13, false),
|
||||
map(0x11, MINECRAFT_1_14, false),
|
||||
map(0x12, MINECRAFT_1_16, false),
|
||||
map(0x11, MINECRAFT_1_17, false),
|
||||
map(0x13, MINECRAFT_1_19, false),
|
||||
map(0x14, MINECRAFT_1_19_1, false),
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false),
|
||||
map(0x1A, MINECRAFT_1_20_5, false));
|
||||
map(0x12, MINECRAFT_1_21, false),
|
||||
map(0x14, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(SetPlayerOnGround.class, SetPlayerOnGround::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
@@ -493,7 +555,61 @@ public enum FallbackPacketRegistry {
|
||||
map(0x17, MINECRAFT_1_19_4, false),
|
||||
map(0x19, MINECRAFT_1_20_2, false),
|
||||
map(0x1A, MINECRAFT_1_20_3, false),
|
||||
map(0x1D, MINECRAFT_1_20_5, false));
|
||||
map(0x1D, MINECRAFT_1_20_5, false),
|
||||
map(0x1F, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(SetPlayerPositionPacket.class, SetPlayerPositionPacket::new,
|
||||
map(0x04, MINECRAFT_1_7_2, false),
|
||||
map(0x0C, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_12, false),
|
||||
map(0x0D, MINECRAFT_1_12_1, false),
|
||||
map(0x10, MINECRAFT_1_13, false),
|
||||
map(0x11, MINECRAFT_1_14, false),
|
||||
map(0x12, MINECRAFT_1_16, false),
|
||||
map(0x11, MINECRAFT_1_17, false),
|
||||
map(0x13, MINECRAFT_1_19, false),
|
||||
map(0x14, MINECRAFT_1_19_1, false),
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false),
|
||||
map(0x1A, MINECRAFT_1_20_5, false),
|
||||
map(0x1C, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(SetPlayerRotationPacket.class, SetPlayerRotationPacket::new,
|
||||
map(0x05, MINECRAFT_1_7_2, false),
|
||||
map(0x0E, MINECRAFT_1_9, false),
|
||||
map(0x10, MINECRAFT_1_12, false),
|
||||
map(0x0F, MINECRAFT_1_12_1, false),
|
||||
map(0x12, MINECRAFT_1_13, false),
|
||||
map(0x13, MINECRAFT_1_14, false),
|
||||
map(0x14, MINECRAFT_1_16, false),
|
||||
map(0x13, MINECRAFT_1_17, false),
|
||||
map(0x15, MINECRAFT_1_19, false),
|
||||
map(0x16, MINECRAFT_1_19_1, false),
|
||||
map(0x15, MINECRAFT_1_19_3, false),
|
||||
map(0x16, MINECRAFT_1_19_4, false),
|
||||
map(0x18, MINECRAFT_1_20_2, false),
|
||||
map(0x19, MINECRAFT_1_20_3, false),
|
||||
map(0x1C, MINECRAFT_1_20_5, false),
|
||||
map(0x1E, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
|
||||
map(0x06, MINECRAFT_1_7_2, false),
|
||||
map(0x0D, MINECRAFT_1_9, false),
|
||||
map(0x0F, MINECRAFT_1_12, false),
|
||||
map(0x0E, MINECRAFT_1_12_1, false),
|
||||
map(0x11, MINECRAFT_1_13, false),
|
||||
map(0x12, MINECRAFT_1_14, false),
|
||||
map(0x13, MINECRAFT_1_16, false),
|
||||
map(0x12, MINECRAFT_1_17, false),
|
||||
map(0x14, MINECRAFT_1_19, false),
|
||||
map(0x15, MINECRAFT_1_19_1, false),
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false),
|
||||
map(0x1B, MINECRAFT_1_20_5, false),
|
||||
map(0x1D, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(ConfirmTeleportationPacket.class, ConfirmTeleportationPacket::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(TransactionPacket.class, TransactionPacket::new,
|
||||
map(0x0F, MINECRAFT_1_7_2, false),
|
||||
map(0x05, MINECRAFT_1_9, false),
|
||||
@@ -508,7 +624,22 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_4, false),
|
||||
map(0x23, MINECRAFT_1_20_2, false),
|
||||
map(0x24, MINECRAFT_1_20_3, false),
|
||||
map(0x27, MINECRAFT_1_20_5, false));
|
||||
map(0x27, MINECRAFT_1_20_5, false),
|
||||
map(0x29, MINECRAFT_1_21_2, false));
|
||||
serverbound.register(AnimationPacket.class, AnimationPacket::new,
|
||||
map(0x0A, MINECRAFT_1_7_2, false),
|
||||
map(0x1A, MINECRAFT_1_9, false),
|
||||
map(0x1D, MINECRAFT_1_12, false),
|
||||
map(0x27, MINECRAFT_1_13, false),
|
||||
map(0x2A, MINECRAFT_1_14, false),
|
||||
map(0x2B, MINECRAFT_1_16, false),
|
||||
map(0x2C, MINECRAFT_1_16_2, false),
|
||||
map(0x2E, MINECRAFT_1_19, false),
|
||||
map(0x2F, MINECRAFT_1_19_1, false),
|
||||
map(0x32, MINECRAFT_1_20_2, false),
|
||||
map(0x33, MINECRAFT_1_20_3, false),
|
||||
map(0x36, MINECRAFT_1_20_5, false),
|
||||
map(0x38, MINECRAFT_1_21_2, false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -525,30 +656,28 @@ public enum FallbackPacketRegistry {
|
||||
return (direction == Direction.SERVERBOUND ? serverbound : clientbound).getProtocolRegistry(version);
|
||||
}
|
||||
|
||||
public static class PacketRegistry {
|
||||
private final Map<ProtocolVersion, ProtocolRegistry> versions;
|
||||
public static final class PacketRegistry {
|
||||
private final Map<ProtocolVersion, ProtocolRegistry> registries = new EnumMap<>(ProtocolVersion.class);
|
||||
|
||||
PacketRegistry() {
|
||||
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
|
||||
for (final ProtocolVersion version : ProtocolVersion.values()) {
|
||||
if (!version.isUnknown()) {
|
||||
mutableVersions.put(version, new ProtocolRegistry(version));
|
||||
for (final ProtocolVersion protocolVersion : ProtocolVersion.values()) {
|
||||
if (!protocolVersion.isUnknown()) {
|
||||
registries.put(protocolVersion, new ProtocolRegistry(protocolVersion));
|
||||
}
|
||||
}
|
||||
this.versions = Collections.unmodifiableMap(mutableVersions);
|
||||
}
|
||||
|
||||
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
|
||||
final ProtocolRegistry registry = versions.get(version);
|
||||
private @NotNull ProtocolRegistry getProtocolRegistry(final @NotNull ProtocolVersion protocolVersion) {
|
||||
final ProtocolRegistry registry = registries.get(protocolVersion);
|
||||
if (registry == null) {
|
||||
throw new IllegalArgumentException("Could not find data for protocol version " + version);
|
||||
throw new IllegalArgumentException("Could not find data for protocol version " + protocolVersion);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
<P extends FallbackPacket> void register(final Class<P> clazz,
|
||||
final Supplier<P> packetSupplier,
|
||||
final PacketMapping @NotNull ... mappings) {
|
||||
private <T extends FallbackPacket> void register(final @NotNull Class<T> clazz,
|
||||
final @NotNull Supplier<T> supplier,
|
||||
final PacketMapping @NotNull ... mappings) {
|
||||
if (mappings.length == 0) {
|
||||
throw new IllegalArgumentException("At least one mapping must be provided.");
|
||||
}
|
||||
@@ -558,21 +687,21 @@ public enum FallbackPacketRegistry {
|
||||
final PacketMapping next = (i + 1 < mappings.length) ? mappings[i + 1] : current;
|
||||
|
||||
final ProtocolVersion from = current.protocolVersion;
|
||||
final ProtocolVersion to = getProtocolVersion(current, next);
|
||||
final ProtocolVersion to = current == next ? LATEST_VERSION : next.protocolVersion;
|
||||
|
||||
for (final ProtocolVersion protocol : EnumSet.range(from, to)) {
|
||||
if (protocol == to && next != current) {
|
||||
for (final ProtocolVersion protocolVersion : EnumSet.range(from, to)) {
|
||||
if (protocolVersion == to && next != current) {
|
||||
break;
|
||||
}
|
||||
|
||||
final ProtocolRegistry registry = versions.get(protocol);
|
||||
final ProtocolRegistry registry = registries.get(protocolVersion);
|
||||
if (registry == null) {
|
||||
throw new IllegalArgumentException("Unknown protocol version " + from);
|
||||
throw new IllegalArgumentException("Unknown protocol version " + protocolVersion);
|
||||
}
|
||||
|
||||
if (registry.packetIdToSupplier.containsKey(current.id)) {
|
||||
if (registry.packetIdToSupplier.containsKey(current.packetId)) {
|
||||
throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName()
|
||||
+ " with id " + current.id + " for " + registry.protocolVersion
|
||||
+ " with id " + current.packetId + " for " + registry.protocolVersion
|
||||
+ " because another packet is already registered");
|
||||
}
|
||||
|
||||
@@ -582,18 +711,12 @@ public enum FallbackPacketRegistry {
|
||||
}
|
||||
|
||||
if (!current.encodeOnly) {
|
||||
registry.packetIdToSupplier.put(current.id, packetSupplier);
|
||||
registry.packetIdToSupplier.put(current.packetId, supplier);
|
||||
}
|
||||
registry.packetClassToId.put(clazz, current.id);
|
||||
registry.packetClassToId.put(clazz, current.packetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
|
||||
final @NotNull PacketMapping next) {
|
||||
return current == next ? LATEST_VERSION : next.protocolVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -604,43 +727,38 @@ public enum FallbackPacketRegistry {
|
||||
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
|
||||
new HashMap<>(16, 0.5f);
|
||||
|
||||
public FallbackPacket createPacket(final int id) {
|
||||
final Supplier<? extends FallbackPacket> supplier = packetIdToSupplier.get(id);
|
||||
|
||||
if (supplier == null) {
|
||||
return null;
|
||||
}
|
||||
return supplier.get();
|
||||
public FallbackPacket createPacket(final int packetId) {
|
||||
final var supplier = packetIdToSupplier.get(packetId);
|
||||
return supplier == null ? null : supplier.get();
|
||||
}
|
||||
|
||||
public int getPacketId(final @NotNull FallbackPacket packet) {
|
||||
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
|
||||
|
||||
if (id == Integer.MIN_VALUE) {
|
||||
throw new IllegalArgumentException("Could not find packet");
|
||||
public int getPacketId(final @NotNull Class<? extends FallbackPacket> clazz) {
|
||||
final int packetId = packetClassToId.getOrDefault(clazz, -1);
|
||||
if (packetId == -1) {
|
||||
throw new IllegalArgumentException("Could not find packet ID for " + clazz.getSimpleName());
|
||||
}
|
||||
return id;
|
||||
return packetId;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@ToString
|
||||
public static final class PacketMapping {
|
||||
private final int id;
|
||||
private final int packetId;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final boolean encodeOnly;
|
||||
|
||||
PacketMapping(final int id,
|
||||
PacketMapping(final int packetId,
|
||||
final ProtocolVersion protocolVersion,
|
||||
final boolean encodeOnly) {
|
||||
this.id = id;
|
||||
this.packetId = packetId;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.encodeOnly = encodeOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull PacketMapping map(final int id,
|
||||
private static @NotNull PacketMapping map(final int packetId,
|
||||
final ProtocolVersion protocolVersion,
|
||||
final boolean encodeOnly) {
|
||||
return new PacketMapping(id, protocolVersion, encodeOnly);
|
||||
return new PacketMapping(packetId, protocolVersion, encodeOnly);
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import io.netty.util.collection.IntObjectHashMap;
|
||||
import io.netty.util.collection.IntObjectMap;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.ID_TO_PROTOCOL_CONSTANT;
|
||||
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/PacketSnapshot.java
|
||||
public final class FallbackPacketSnapshot implements FallbackPacket {
|
||||
private final IntObjectMap<Integer> mappings = new IntObjectHashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
|
||||
private final IntObjectMap<byte[]> cachedBytes = new IntObjectHashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
|
||||
@Getter
|
||||
private final Class<? extends FallbackPacket> originalPacketClass;
|
||||
|
||||
public FallbackPacketSnapshot(final @NotNull FallbackPacket originalPacket) {
|
||||
this.originalPacketClass = originalPacket.getClass();
|
||||
|
||||
final IntObjectMap<Integer> hashedData = new IntObjectHashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
|
||||
|
||||
for (final ProtocolVersion protocolVersion : ID_TO_PROTOCOL_CONSTANT.values()) {
|
||||
// Allocate a buffer for each protocol version
|
||||
final ByteBuf byteBuf = Unpooled.buffer();
|
||||
try {
|
||||
try {
|
||||
originalPacket.encode(byteBuf, protocolVersion);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not encode packet {} for version {}: {}",
|
||||
originalPacket, protocolVersion, throwable);
|
||||
break;
|
||||
}
|
||||
|
||||
final Integer protocol = protocolVersion.getProtocol();
|
||||
final Integer hash = byteBuf.hashCode();
|
||||
final Integer hashed = hashedData.getOrDefault(hash, -1);
|
||||
|
||||
if (hashed != -1) {
|
||||
mappings.put(protocol, hashed);
|
||||
} else {
|
||||
hashedData.put(hash, protocol);
|
||||
mappings.put(protocol, protocol);
|
||||
// Cache the raw bytes of the encoded packet
|
||||
final byte[] bytes = new byte[byteBuf.readableBytes()];
|
||||
byteBuf.readBytes(bytes);
|
||||
cachedBytes.put(protocol, bytes);
|
||||
}
|
||||
} finally {
|
||||
// Make sure to release the buffer to avoid memory leaks
|
||||
byteBuf.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
final Integer hash = mappings.get(protocolVersion.getProtocol());
|
||||
if (hash == null) {
|
||||
throw ProtocolUtil.DEBUG ? new EncoderException("Unable to find hash") : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
final byte[] bytes = cachedBytes.get(hash);
|
||||
if (bytes == null) {
|
||||
throw ProtocolUtil.DEBUG ? new EncoderException("Unable to find cached packet") : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
byteBuf.writeBytes(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.IntBinaryTag;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.event.impl.CaptchaGenerationStartEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.item.ItemType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
private final Random RANDOM = new Random();
|
||||
|
||||
public static final int PLAYER_ENTITY_ID = RANDOM.nextInt();
|
||||
public static final int VEHICLE_ENTITY_ID = RANDOM.nextInt();
|
||||
|
||||
public static final BlockType[] POSSIBLE_BLOCK_TYPES = BlockType.values();
|
||||
public final FallbackPacket[] BLOCKS_PACKETS = new FallbackPacket[POSSIBLE_BLOCK_TYPES.length];
|
||||
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (vanilla 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 PLATFORM_Y_POSITION = 1 + RANDOM.nextInt(255); // 255 is the maximum Y position
|
||||
public final int IN_AIR_Y_POSITION = 3000 + RANDOM.nextInt(500); // Random high altitude
|
||||
|
||||
public final int FIRST_TELEPORT_ID = RANDOM.nextInt();
|
||||
public final int SECOND_TELEPORT_ID = RANDOM.nextInt();
|
||||
public final int PRE_JOIN_KEEP_ALIVE_ID = RANDOM.nextInt();
|
||||
|
||||
private final int MAP_SLOT = RANDOM.nextInt(9);
|
||||
|
||||
public static final FallbackPacket CAPTCHA_SET_CONTAINER_SLOT = new SetContainerSlotPacket(
|
||||
0, 36 + MAP_SLOT, 1, ItemType.FILLED_MAP, CompoundBinaryTag.builder()
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map id
|
||||
.build());
|
||||
public final static FallbackPacket CAPTCHA_HELD_ITEM_SLOT = new SetHeldItemPacket(MAP_SLOT);
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new PlayerAbilitiesPacket(0x00, 0, 0);
|
||||
public final FallbackPacket NO_MOVE_ABILITIES = new PlayerAbilitiesPacket(0x02, 0, 0);
|
||||
public final FallbackPacket NO_MOVE_ABILITIES_BEDROCK = new PlayerAbilitiesPacket(0x06, 0, 0);
|
||||
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, 10000, SPAWN_Z_POSITION, 0, 90, 0, 0, false, false, true));
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
|
||||
public final FallbackPacket PRE_JOIN_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(PRE_JOIN_KEEP_ALIVE_ID));
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_20 = new FallbackPacket[] {
|
||||
new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null))};
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_21_2 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21_2);
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
|
||||
public final static FallbackPacket INVALID_HELD_ITEM_SLOT = new SetHeldItemPacket(-1);
|
||||
public final FallbackPacket RANDOM_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(RANDOM.nextInt()));
|
||||
public static final FallbackPacket SPAWN_BOAT_ENTITY = new FallbackPacketSnapshot(new SpawnEntityPacket(
|
||||
VEHICLE_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
|
||||
0, 0, 0, 0));
|
||||
public static final FallbackPacket SPAWN_MINECART_ENTITY = new FallbackPacketSnapshot(new SpawnEntityPacket(
|
||||
VEHICLE_ENTITY_ID, EntityType.MINECART, SPAWN_X_POSITION, IN_AIR_Y_POSITION - 16, SPAWN_Z_POSITION,
|
||||
0, 0, 0, 0));
|
||||
public static final FallbackPacket REMOVE_VEHICLE = new RemoveEntitiesPacket(VEHICLE_ENTITY_ID);
|
||||
public static final FallbackPacket SET_VEHICLE_PASSENGERS = new FallbackPacketSnapshot(
|
||||
new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
|
||||
|
||||
public FallbackPacket loginSuccess;
|
||||
public FallbackPacket welcomeMessage;
|
||||
public FallbackPacket enterCodeMessage;
|
||||
public FallbackPacket incorrectCaptcha;
|
||||
public static FallbackPacket joinGame;
|
||||
public FallbackPacket defaultSpawnPosition;
|
||||
public FallbackPacket spawnPosition;
|
||||
public FallbackPacket fallStartPosition;
|
||||
public FallbackPacket fallStartPositionLegacy;
|
||||
public static FallbackPacket transferToOrigin;
|
||||
public FallbackPacket blacklisted;
|
||||
public FallbackPacket alreadyQueued;
|
||||
public FallbackPacket alreadyVerifying;
|
||||
public FallbackPacket reconnectedTooFast;
|
||||
public FallbackPacket protocolBlacklisted;
|
||||
public FallbackPacket invalidUsername;
|
||||
public FallbackPacket tooManyOnlinePerIP;
|
||||
public FallbackPacket[] xpCountdown;
|
||||
public FallbackPacket updateTime;
|
||||
|
||||
public static int maxMovementTick, dynamicSpawnYPosition, maxTotalPacketsSent;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public void prepare() {
|
||||
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
|
||||
Sonar.get().getLogger().info("Preloading all registered packets...");
|
||||
FallbackPacketRegistry.values();
|
||||
|
||||
// Prepare LoginSuccess packet with capped username to 16 characters
|
||||
final UUID uuid = UUID.randomUUID();
|
||||
String username = Sonar.get().getConfig().getGeneralConfig().getString("verification.cached-username");
|
||||
if (username.length() > 16) {
|
||||
username = username.substring(0, 16);
|
||||
}
|
||||
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(uuid, username, true));
|
||||
|
||||
// Prepare JoinGame packet
|
||||
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
|
||||
Sonar.get().getConfig().getVerification().getGamemode().getId(),
|
||||
-1, 0, 0,
|
||||
RANDOM.nextInt(3), 1, 0, 0,
|
||||
new String[]{"lol"}, "lol", "flat",
|
||||
DimensionType.OVERWORLD, RANDOM.nextLong() & 1337,
|
||||
false, true, false,
|
||||
false, false, false, true));
|
||||
|
||||
// Prepare the gravity check
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
|
||||
|
||||
double motionY = 0, fallDistance = 0;
|
||||
|
||||
for (int i = 0; i < maxMovementTick; i++) {
|
||||
motionY = (motionY - 0.1) * 0.98f;
|
||||
fallDistance += Math.abs(motionY);
|
||||
}
|
||||
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
dynamicSpawnYPosition = PLATFORM_Y_POSITION + (int) Math.ceil(fallDistance);
|
||||
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
|
||||
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
|
||||
0, 0, FIRST_TELEPORT_ID, 0, false, false, true));
|
||||
fallStartPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition - IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
|
||||
0, -90, SECOND_TELEPORT_ID, 1 << 1, false, false, true));
|
||||
fallStartPositionLegacy = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0, -90, 0, 0, false, false, true));
|
||||
|
||||
// Prepare collision platform positions
|
||||
final int length = BLOCKS_PER_ROW / 2;
|
||||
for (int i = 0; i < BLOCKS_PACKETS.length; i++) {
|
||||
final BlockUpdate[] blocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
|
||||
int index = 0;
|
||||
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
|
||||
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
|
||||
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
|
||||
x + length, PLATFORM_Y_POSITION, z + length, 0, 0);
|
||||
blocks[index++] = new BlockUpdate(position, POSSIBLE_BLOCK_TYPES[i]);
|
||||
}
|
||||
}
|
||||
BLOCKS_PACKETS[i] = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, blocks));
|
||||
}
|
||||
|
||||
// Prepare disconnect packets during login
|
||||
blacklisted = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
|
||||
alreadyVerifying = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true));
|
||||
alreadyQueued = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued(), true));
|
||||
protocolBlacklisted = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true));
|
||||
reconnectedTooFast = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true));
|
||||
invalidUsername = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getVerification().getInvalidUsername(), true));
|
||||
tooManyOnlinePerIP = new FallbackPacketSnapshot(new DisconnectPacket(Sonar.get().getConfig().getTooManyOnlinePerIp(), true));
|
||||
|
||||
// Prepare transfer packet
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("verification.transfer.enabled")) {
|
||||
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("verification.transfer.destination-host"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getInt("verification.transfer.destination-port")));
|
||||
} else {
|
||||
transferToOrigin = null;
|
||||
}
|
||||
|
||||
// Prepare update time packet
|
||||
final int timeOfDay = Sonar.get().getConfig().getVerification().getTimeOfDay();
|
||||
if (timeOfDay != 1000) {
|
||||
updateTime = new UpdateTimePacket(0L, timeOfDay, false);
|
||||
} else {
|
||||
updateTime = null;
|
||||
}
|
||||
|
||||
// If the welcome message is empty, we don't need to send a message to the player
|
||||
final String welcome = Sonar.get().getConfig().getMessagesConfig().getString("verification.welcome");
|
||||
if (welcome.isEmpty()) {
|
||||
welcomeMessage = null;
|
||||
} else {
|
||||
welcomeMessage = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(welcome,
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
}
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|
||||
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
// Prepare CAPTCHA messages
|
||||
enterCodeMessage = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.captcha.enter"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
incorrectCaptcha = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.captcha.incorrect"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
|
||||
// Prepare countdown
|
||||
xpCountdown = new FallbackPacket[Sonar.get().getConfig().getVerification().getMap().getMaxDuration() / 1000];
|
||||
|
||||
for (int i = 0; i < xpCountdown.length; i++) {
|
||||
final float bar = (float) i / xpCountdown.length;
|
||||
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, 0));
|
||||
}
|
||||
|
||||
// Update the CAPTCHA generator if necessary
|
||||
final CaptchaGenerationStartEvent generationStartEvent = new CaptchaGenerationStartEvent(
|
||||
Sonar.get().getFallback().getCaptchaGenerator());
|
||||
Sonar.get().getEventManager().publish(generationStartEvent);
|
||||
|
||||
if (generationStartEvent.getCaptchaGenerator() == null
|
||||
|| generationStartEvent.getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
|
||||
Sonar.get().getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(
|
||||
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
|
||||
} else {
|
||||
Sonar.get().getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
|
||||
}
|
||||
|
||||
// Prepare CAPTCHA answers
|
||||
CaptchaPreparer.prepare();
|
||||
} else {
|
||||
// Throw away if not needed
|
||||
enterCodeMessage = null;
|
||||
incorrectCaptcha = null;
|
||||
xpCountdown = null;
|
||||
Sonar.get().getFallback().setCaptchaGenerator(null);
|
||||
}
|
||||
|
||||
maxTotalPacketsSent = maxMovementTick + 2
|
||||
+ (xpCountdown == null ? 0 : xpCountdown.length) * 20 + 5
|
||||
+ Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets() * 4
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries()
|
||||
+ 150 /* some arbitrary leeway */;
|
||||
}
|
||||
|
||||
public static FallbackPacket[] getRegistryPackets(final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21_2)) {
|
||||
return REGISTRY_SYNC_1_21_2;
|
||||
}
|
||||
if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_21)) {
|
||||
return REGISTRY_SYNC_1_21;
|
||||
}
|
||||
if (protocolVersion.greaterThanOrEquals(ProtocolVersion.MINECRAFT_1_20_5)) {
|
||||
return REGISTRY_SYNC_1_20_5;
|
||||
}
|
||||
return REGISTRY_SYNC_1_20;
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.block;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
// TODO: load mappings from a separate file
|
||||
@Getter
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum BlockType {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/
|
||||
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
|
||||
//STONE(protocolVersion -> 1, 1),
|
||||
ENCHANTMENT_TABLE(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 116;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 4612;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 4613;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 5116;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 5132;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 5136;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 5333;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 5719;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 7159;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 7385;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 7389;
|
||||
}
|
||||
return 7619;
|
||||
}, protocolVersion -> (double) 0.75f),
|
||||
TRAPDOOR(protocolVersion -> {
|
||||
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_7_6)) {
|
||||
return 96;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 167;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 6509;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 6510;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 7016;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 7552;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 7556;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 7802;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 8293;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 9937;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 10269;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
|
||||
return 10273;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 10414;
|
||||
}
|
||||
return 10749;
|
||||
}, protocolVersion -> 0.1875),
|
||||
END_PORTAL_FRAME(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 120;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 4633;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 4634;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 5137;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 5153;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 5157;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 5358;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 5744;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 7184;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 7410;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 7414;
|
||||
}
|
||||
return 7644;
|
||||
}, protocolVersion -> 0.8125),
|
||||
DAYLIGHT_SENSOR(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 151;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 5651;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 5652;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 6158;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 6694;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 6698;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 6916;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 7327;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 8811;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 9063;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
|
||||
return 9067;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 9207;
|
||||
}
|
||||
return 9462;
|
||||
}, protocolVersion -> 0.375),
|
||||
COBBLESTONE_WALL(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 139;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 5196;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 5197;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 5700;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 5660;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 5664;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 5866;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 6252;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 7692;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 7918;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 7922;
|
||||
}
|
||||
return 8152;
|
||||
}, protocolVersion -> 1.5),
|
||||
STONE_SLABS(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 44;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 7296;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 7297;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 7809;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 8345;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 8349;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 8595;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 9092;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 10748;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 11086;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
|
||||
return 11090;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 11231;
|
||||
}
|
||||
return 11566;
|
||||
}, protocolVersion -> 0.5),
|
||||
WHITE_CARPET(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 171;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 6823;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 6824;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 7330;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_1)) {
|
||||
return 7866;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 7870;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 8116;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 8607;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 10251;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 10583;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20)) {
|
||||
return 10587;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 10728;
|
||||
}
|
||||
return 11063;
|
||||
}, protocolVersion -> protocolVersion.compareTo(MINECRAFT_1_8) < 0 ? 0 : 0.0625);
|
||||
|
||||
private final Function<ProtocolVersion, Integer> id;
|
||||
private final Function<ProtocolVersion, Double> blockHeight;
|
||||
}
|
||||
+5
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -19,18 +19,17 @@ package xyz.jonesdev.sonar.common.fallback.protocol.block;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
public final class BlockUpdate {
|
||||
private final @NotNull BlockPosition position;
|
||||
private final BlockType type;
|
||||
private final BlockPosition position;
|
||||
private final BlockType blockType;
|
||||
private final int legacyBlockState, blockState;
|
||||
|
||||
public BlockUpdate(final @NotNull BlockPosition position, final BlockType type) {
|
||||
public BlockUpdate(final @NotNull BlockPosition position, final BlockType blockType) {
|
||||
this.position = position;
|
||||
this.type = type;
|
||||
this.blockType = blockType;
|
||||
final int x = position.getX() - (position.getChunkX() << 4);
|
||||
final int y = position.getY();
|
||||
final int z = position.getZ() - (position.getChunkZ() << 4);
|
||||
@@ -39,7 +38,6 @@ public final class BlockUpdate {
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public static final class BlockPosition {
|
||||
private final int x, y, z;
|
||||
+12
-24
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -18,10 +18,8 @@
|
||||
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;
|
||||
|
||||
@@ -30,36 +28,26 @@ 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 CODEC_1_21;
|
||||
public final @NotNull CompoundBinaryTag OLD_CODEC;
|
||||
public final CompoundBinaryTag CODEC_1_16;
|
||||
public final CompoundBinaryTag CODEC_1_16_2;
|
||||
public final CompoundBinaryTag CODEC_1_18_2;
|
||||
public final CompoundBinaryTag CODEC_1_19;
|
||||
public final CompoundBinaryTag CODEC_1_19_1;
|
||||
public final CompoundBinaryTag CODEC_1_19_4;
|
||||
public final CompoundBinaryTag CODEC_1_20;
|
||||
public final CompoundBinaryTag CODEC_1_21;
|
||||
public final CompoundBinaryTag CODEC_1_21_2;
|
||||
|
||||
static {
|
||||
CODEC_1_16 = getCodec("codec_1_16.nbt");
|
||||
CODEC_1_16_2 = getCodec("codec_1_16_2.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");
|
||||
CODEC_1_21 = getCodec("codec_1_21.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);
|
||||
CODEC_1_21_2 = getCodec("codec_1_21_2.nbt");
|
||||
}
|
||||
|
||||
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* Copyright (C) 2024 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
|
||||
@@ -19,14 +19,14 @@ 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;
|
||||
private final int id;
|
||||
private final CompoundBinaryTag tag;
|
||||
public enum DimensionType {
|
||||
OVERWORLD("minecraft:overworld", 0, 0),
|
||||
THE_NETHER("minecraft:the_nether", -1, 1),
|
||||
THE_END("minecraft:the_end", 1, 2);
|
||||
|
||||
private final String key;
|
||||
private final int legacyId, id;
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
// TODO: load mappings from a separate file
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum EntityType {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
BOAT(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 1;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_14_4)) {
|
||||
return 5;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 6;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 7;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 8;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20_3)) {
|
||||
return 9;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 10;
|
||||
}
|
||||
return 85; // 1.21.2 split the boat type in id registries.
|
||||
}),
|
||||
MINECART(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 10;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_14_4)) {
|
||||
return 41;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 42;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 45;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 50;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 53;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 54;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20_2)) {
|
||||
return 64;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20_3)) {
|
||||
return 65;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 69;
|
||||
}
|
||||
return 82;
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> id;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.item;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
// TODO: load mappings from a separate file
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ItemType {
|
||||
// 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
|
||||
FILLED_MAP(protocolVersion -> {
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_12_2)) {
|
||||
return 358;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_1)) {
|
||||
return 608;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_13_2)) {
|
||||
return 613;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_15_2)) {
|
||||
return 671;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_16_4)) {
|
||||
return 733;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_18_2)) {
|
||||
return 847;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_1)) {
|
||||
return 886;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_3)) {
|
||||
return 914;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_19_4)) {
|
||||
return 937;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20_2)) {
|
||||
return 941;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_20_3)) {
|
||||
return 979;
|
||||
}
|
||||
if (protocolVersion.lessThanOrEquals(MINECRAFT_1_21)) {
|
||||
return 982;
|
||||
}
|
||||
return 1022;
|
||||
}, protocolVersion -> protocolVersion.greaterThanOrEquals(MINECRAFT_1_21_2) ? 36 : 26);
|
||||
|
||||
private final Function<ProtocolVersion, Integer> id;
|
||||
private final Function<ProtocolVersion, Integer> components;
|
||||
}
|
||||
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user