Compare commits

...
10 Commits
Author SHA1 Message Date
Jones 3c73ecf60f Release 2.1.10 (#392) 2024-10-06 14:15:51 +02:00
Jones c116d7b556 Reduce the usage of static imports 2024-10-05 11:01:47 +02:00
Jones 057cda6c7a Remove VerifiedPlayerController#estimatedSize 2024-10-05 10:51:26 +02:00
Jones 3a97a1f949 Add easier API method for verifying players 2024-10-05 10:50:30 +02:00
Jones 69b20769de Add more stability checks to the database 2024-10-05 10:47:06 +02:00
Jones 9b87fde7c6 Implement missing view distance check 2024-10-05 09:38:13 +02:00
Jones b939767ab9 Fix translation error in Spanish translations 2024-10-01 17:04:13 +02:00
Jones d5a8cd5346 Tell user how to disable the welcome message 2024-10-01 17:03:58 +02:00
Jones aa4e4662bb Fix possible position issue with vehicles 2024-10-01 17:01:02 +02:00
dependabot[bot] 29187d4248 Bump org.jetbrains:annotations from 24.1.0 to 25.0.0 (#390)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.1.0 to 25.0.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/24.1.0...25.0.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:37:16 +02:00
26 changed files with 85 additions and 85 deletions

No files matched your search

@@ -31,6 +31,7 @@ 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;
@@ -45,8 +46,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
@Getter
private final Set<String> cache = Collections.synchronizedSet(new HashSet<>());
private @Nullable ConnectionSource connectionSource;
@@ -54,6 +53,7 @@ public final class VerifiedPlayerController {
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();
@@ -95,7 +95,6 @@ public final class VerifiedPlayerController {
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
// Create database table
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException ignored) {
@@ -111,14 +110,16 @@ public final class VerifiedPlayerController {
// Make sure to run the clean task and the caching task in the same thread
// https://github.com/jonesdevelopment/sonar/issues/150
DB_UPDATE_SERVICE.execute(() -> {
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);
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) {
@@ -137,16 +138,17 @@ public final class VerifiedPlayerController {
} 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.
* 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; // convert to ms
final long timestamp = Instant.now().minus(maximumAge, ChronoUnit.DAYS).getEpochSecond() * 1000L;
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
@@ -175,34 +177,36 @@ public final class VerifiedPlayerController {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
return;
}
updateService.execute(() -> {
if (connectionSource != null) {
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
.eq("fingerprint", fingerprint)
.query();
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
.eq("fingerprint", fingerprint)
.query();
if (verifiedPlayer != null) {
for (final VerifiedPlayer player : verifiedPlayer) {
dao.delete(player);
if (verifiedPlayer != null) {
for (final VerifiedPlayer player : verifiedPlayer) {
dao.delete(player);
}
}
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
});
}
/**
* First, cache the player locally and then,
* secondly, asynchronously add the player to the database.
*
* @param player VerifiedPlayer model
* 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());
@@ -212,39 +216,27 @@ public final class VerifiedPlayerController {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
return;
}
try {
dao.createIfNotExists(player);
} catch (SQLException exception) {
exception.printStackTrace(System.err);
updateService.execute(() -> {
if (connectionSource != null) {
try {
dao.createIfNotExists(player);
} catch (SQLException exception) {
exception.printStackTrace(System.err);
}
}
});
}
/**
* Returns the number of verified IP addresses
*
* @return Estimated size of the local cache
*/
public int estimatedSize() {
return cache.size();
}
/**
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
*/
public synchronized void clearAll() {
public void clearAll() {
cache.clear();
// Delete the entire table from the database, if necessary
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
if (connectionSource != null
&& cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
@@ -66,7 +66,7 @@ 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().getChatNotificationHandler().handleNotification();
@@ -118,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);
@@ -27,10 +27,9 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import java.util.Collections;
import java.util.List;
import static java.util.Collections.emptyList;
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
@Override
@@ -55,7 +54,7 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
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 Collections.emptyList();
}
return getCachedTabSuggestions(args);
}
@@ -26,7 +26,7 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import static java.util.Collections.emptyList;
import java.util.Collections;
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
@@ -48,7 +48,7 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
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 Collections.emptyList();
}
return getCachedTabSuggestions(args);
}
@@ -38,10 +38,6 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.FONTS;
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.STANDARD_FONT_SIZE;
@Getter
@RequiredArgsConstructor
public final class StandardCaptchaGenerator implements CaptchaGenerator {
@@ -92,7 +88,7 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
private @NotNull BufferedImage createBackgroundImage() {
// Create a new image buffer with a 3-byte color spectrum
final BufferedImage image = new BufferedImage(width, height, TYPE_3BYTE_BGR);
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);
@@ -128,7 +124,7 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
for (int i = 0; i < answer.length; i++) {
// Create a glyph vector for the character with a random font
final Font font = FONTS[RANDOM.nextInt(FONTS.length)];
final Font font = StandardTTFFontProvider.FONTS[RANDOM.nextInt(StandardTTFFontProvider.FONTS.length)];
glyphs[i] = font.createGlyphVector(ctx, String.valueOf(answer[i]));
}
@@ -139,7 +135,7 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
.mapToDouble(glyph -> glyph.getLogicalBounds().getWidth() * scalingXY - 1)
.sum();
double beginX = Math.max(Math.min(width / 2D - totalWidth / 2D, totalWidth), 0);
double beginY = (height + STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
double beginY = (height + StandardTTFFontProvider.STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
// Draw each glyph one by one
for (final GlyphVector glyph : glyphs) {
@@ -21,12 +21,12 @@ 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.exception.QuietDecoderException;
import java.util.List;
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
@@ -42,7 +42,7 @@ public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
}
// Skip any runs of 0x00 we might find
final int packetStart = byteBuf.forEachByte(FIND_NON_NUL);
final int packetStart = byteBuf.forEachByte(ByteProcessor.FIND_NON_NUL);
if (packetStart == -1) {
return;
}
@@ -184,10 +184,10 @@ public class FallbackPreparer {
teleportMinecart = new TeleportEntityPacket(
VEHICLE_ENTITY_ID, SPAWN_X_POSITION, IN_VOID_Y_POSITION, SPAWN_Z_POSITION, false);
spawnBoatEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(VEHICLE_ENTITY_ID,
EntityType.BOAT, RANDOM.nextInt(16), IN_AIR_Y_POSITION, RANDOM.nextInt(16),
EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, 0, 0, 0));
spawnMinecartEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(VEHICLE_ENTITY_ID,
EntityType.MINECART, RANDOM.nextInt(16), IN_AIR_Y_POSITION, RANDOM.nextInt(16),
EntityType.MINECART, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, 0, 0, 0));
setPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
@@ -126,6 +126,8 @@ public final class FallbackPreJoinHandler extends FallbackVerificationHandler {
} else if (packet instanceof ClientInformationPacket) {
final ClientInformationPacket clientInformation = (ClientInformationPacket) packet;
checkState(clientInformation.getViewDistance() >= 2,
"view distance: " + clientInformation.getViewDistance());
// Ensure that the client locale is correct
validateClientLocale(clientInformation.getLocale());
// Check if the player sent an unused bit flag in the skin section
@@ -130,7 +130,7 @@ public final class GlobalSonarStatistics implements SonarStatistics {
@Override
public int getTotalPlayersVerified() {
return Sonar.get().getVerifiedPlayerController().estimatedSize();
return Sonar.get().getVerifiedPlayerController().getCache().size();
}
@Override
@@ -70,7 +70,7 @@ public final class StatisticsCommand extends Subcommand {
placeholders = new TagResolver.Single[]{
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("verified", DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize())),
Placeholder.unparsed("verified", DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().getCache().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
@@ -95,7 +95,7 @@ public final class VerifiedCommand extends Subcommand {
}
case "clear": {
final int verifiedSize = Sonar.get().getVerifiedPlayerController().estimatedSize();
final int verifiedSize = Sonar.get().getVerifiedPlayerController().getCache().size();
if (verifiedSize == 0) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
@@ -113,7 +113,7 @@ public final class VerifiedCommand extends Subcommand {
}
case "size": {
final int verifiedSize = Sonar.get().getVerifiedPlayerController().estimatedSize();
final int verifiedSize = Sonar.get().getVerifiedPlayerController().getCache().size();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.size"),
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>Zadali jste špatnou odpověď! Zkuste to prosím znovu.'
# Zpráva zobrazená hráči při ověřování
# Nastavením této hodnoty na '' uvítací zprávu zcela vypnete
welcome: '<prefix><green>Jste ověřováni. Prosím, nehýbejte se; jedná se o automatický proces.'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí, zatímco captcha ještě není připravena
@@ -200,6 +200,7 @@ verification:
incorrect: '<prefix><red>Du hast eine falsche Antwort eingegeben! Bitte versuche es erneut.'
# Nachricht, die dem Spieler angezeigt wird, wenn er überprüft wird
# Setzen Sie dies auf '', um die Willkommensnachricht vollständig zu deaktivieren
welcome: '<prefix><green>Sie werden gerade überprüft. Bitte bewegen Sie sich nicht, dies ist ein automatischer Vorgang.'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, während der CAPTCHA noch nicht vorbereitet wurde
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>You have entered a wrong answer! Please try again.'
# Message shown to the player when they are being verified
# Set this to '' to disable the welcome message entirely
welcome: '<prefix><green>You are being verified. Please do not move; this is an automatic process.'
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
@@ -199,7 +199,8 @@ verification:
incorrect: '<prefix><red>¡Has introducido una respuesta incorrecta! Por favor, inténtalo de nuevo.'
# Mensaje que se muestra al jugador cuando está siendo verificado
welcome: '<prefijo><verde>Estás siendo verificado. Por favor, no te muevas; se trata de un proceso automático.'
# Ajústalo a '' para desactivar por completo el mensaje de bienvenida
welcome: '<prefix><green>Estás siendo verificado. Por favor, no te muevas; se trata de un proceso automático.'
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
currently-preparing:
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>Vous avez saisi une mauvaise réponse ! Veuillez réessayer.'
# Message affiché au joueur lorsqu'il est en cours de vérification
# Réglez ce paramètre sur '' pour désactiver complètement le message de bienvenue
welcome: '<prefix><green>Vous êtes en cours de vérification. Ne bougez pas, il s''agit d''un processus automatique.'
# Message de déconnexion affiché lorsque quelqu'un rejoint alors que le CAPTCHA n'a pas été préparé
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>Anda telah memasukkan jawaban yang salah! Silakan coba lagi.'
# Pesan yang ditampilkan kepada pemain ketika mereka sedang diverifikasi
# Atur ini ke '' untuk menonaktifkan pesan selamat datang sepenuhnya
welcome: '<prefix><green>Anda sedang diverifikasi. Jangan bergerak; ini adalah proses otomatis.'
# Pesan putus yang ditampilkan saat seseorang bergabung sementara captcha belum disiapkan
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>თქვენ მიუთითეთ არასწორი კოდი. გთხოვთ ხელახლა ცადოთ.'
# Message shown to the player when they are being verified
# Set this to '' to disable the welcome message entirely
welcome: '<prefix><green>You are being verified. Please do not move; this is an automatic process.'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოვა მაგრამ CAPTCHA არ არის მზად
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>Je hebt een verkeerd antwoord ingevoerd! Probeer het opnieuw.'
# Bericht dat aan de speler wordt getoond als hij wordt geverifieerd
# Stel dit in op '' om het welkomstbericht volledig uit te schakelen
welcome: '<prefix><green>U wordt geverifieerd. Beweeg je niet, dit is een automatisch proces.'
# Verbindingsbericht dat wordt getoond als iemand zich aansluit terwijl de captcha nog niet is voorbereid
@@ -202,6 +202,7 @@ verification:
incorrect: '<prefix><red>Wprowadzono nieprawidłowy kod. Spróbuj ponownie.'
# Wiadomość wyświetlana graczowi podczas weryfikacji
# Ustaw wartość '', aby całkowicie wyłączyć wiadomość powitalną
welcome: '<prefix><green>Jesteś w trakcie weryfikacji. Nie ruszaj się, to proces automatyczny.'
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>Вы ввели неправильный ответ! Пожалуйста, попробуйте еще раз.'
# Сообщение, показываемое игроку при прохождении верификации
# Установите значение '', чтобы полностью отключить приветственное сообщение
welcome: '<prefix><green>Вы проходите верификацию. Пожалуйста, не двигайтесь, это автоматический процесс.'
# Сообщение, которое показывается когда капча еще не подготовлена
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>Yanlış bir cevap girdiniz! Lütfen tekrar deneyin.'
# Doğrulandıklarında oyuncuya gösterilen mesaj
# Karşılama mesajını tamamen devre dışı bırakmak için bunu '' olarak ayarlayın
welcome: '<prefix><green>Doğrulanıyorsunuz. Lütfen hareket etmeyin; bu otomatik bir süreçtir.'
# CAPTCHA henüz hazırlanmamışken birisi katıldığında gösterilen bağlantıyı kesme mesajı
@@ -199,6 +199,7 @@ verification:
incorrect: '<prefix><red>您输入了错误的答案! 请重试.'
# 当玩家正在接受验证时显示的信息
# 将此项设置为''可完全禁用欢迎信息
welcome: '<prefix><green>您正在接受验证. 请不要移动; 这是一个自动过程.'
# 当验证码还在准备的时候显示的踢出消息
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev
version=2.1.9
version=2.1.10
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
+1 -1
View File
@@ -28,7 +28,7 @@ bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
imagefilters = "com.jhlabs:filters:2.0.235-1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
annotations = "org.jetbrains:annotations:24.1.0"
annotations = "org.jetbrains:annotations:25.0.0"
simpleyaml = "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
lombok = "org.projectlombok:lombok:1.18.34"
@@ -23,10 +23,9 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import java.util.Collections;
import java.util.List;
import static java.util.Collections.emptyList;
public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
@Override
@@ -44,7 +43,7 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
public List<String> suggest(final @NotNull Invocation invocation) {
// Do not allow tab completion if the player does not have the required permission
if (!invocation.source().hasPermission("sonar.command")) {
return emptyList();
return Collections.emptyList();
}
return getCachedTabSuggestions(invocation.arguments());
}