mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 17:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75abebde7c | ||
|
|
28b1615b78 | ||
|
|
600275669b | ||
|
|
0c3761be11 | ||
|
|
b7d8ec74d0 | ||
|
|
96efa574e0 | ||
|
|
8db3d0257f | ||
|
|
106a44f78a | ||
|
|
a5872f1b6e |
No files matched your search
+3
-2
@@ -35,7 +35,7 @@ allprojects {
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.30")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
|
||||
val adventureVersion = "4.14.0"
|
||||
val adventureVersion = "4.15.0"
|
||||
|
||||
// adventure
|
||||
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
|
||||
@@ -46,7 +46,7 @@ allprojects {
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.101.Final") // netty
|
||||
compileOnly("io.netty:netty-all:4.1.104.Final") // netty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ tasks {
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
|
||||
exclude("net.kyori.adventure.text.**")
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.10
|
||||
version=2.0.12
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -27,8 +27,10 @@ import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.dependencies.Dependency;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -105,10 +107,13 @@ public final class SonarConfiguration {
|
||||
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private boolean enabled;
|
||||
private boolean drawBackgroundNoise;
|
||||
private Timing timing;
|
||||
private boolean randomizePositions;
|
||||
private boolean randomizeFontSize;
|
||||
private double distortionsFactorX;
|
||||
private double distortionsFactorY;
|
||||
private int randomLinesAmount;
|
||||
private int randomOvalsAmount;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
@@ -116,6 +121,7 @@ public final class SonarConfiguration {
|
||||
private Component enterCode;
|
||||
private Component failedCaptcha;
|
||||
private String enterCodeActionBar;
|
||||
private List<String> fonts;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -169,6 +175,7 @@ public final class SonarConfiguration {
|
||||
private Component alreadyQueued;
|
||||
private Component blacklisted;
|
||||
private Component protocolBlacklisted;
|
||||
private Component currentlyPreparing;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -236,6 +243,7 @@ public final class SonarConfiguration {
|
||||
private String name;
|
||||
private String username;
|
||||
private String password;
|
||||
private int maximumAge;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -371,6 +379,10 @@ public final class SonarConfiguration {
|
||||
"Password for authenticating with the SQL database");
|
||||
database.password = generalConfig.getString("database.password", "");
|
||||
|
||||
generalConfig.getYaml().setComment("database.maximum-age",
|
||||
"How many days should Sonar keep verified players in the database?");
|
||||
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age", 5), 1, 365);
|
||||
|
||||
// Queue
|
||||
generalConfig.getYaml().setComment("queue",
|
||||
"Every new login request will be queued to avoid spam join attacks"
|
||||
@@ -401,15 +413,13 @@ public final class SonarConfiguration {
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.max-movement-ticks",
|
||||
"Maximum number of ticks the player has to fall in order to be allowed to hit the platform");
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity" +
|
||||
".max-movement-ticks", 8),
|
||||
2, 100);
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt(
|
||||
"verification.checks.gravity.max-movement-ticks", 8), 2, 100);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.max-ignored-ticks",
|
||||
"Maximum number of ignored Y movement changes before a player fails verification");
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"
|
||||
, 5),
|
||||
1, 128);
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt(
|
||||
"verification.checks.gravity.max-ignored-ticks", 5), 1, 128);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.gamemode",
|
||||
"The gamemode of the player during verification"
|
||||
@@ -418,31 +428,53 @@ public final class SonarConfiguration {
|
||||
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
|
||||
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible");
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(
|
||||
generalConfig.getString("verification.checks.gravity.gamemode", Verification.Gravity.Gamemode.ADVENTURE.name()).toUpperCase());
|
||||
generalConfig.getString("verification.checks.gravity.gamemode",
|
||||
Verification.Gravity.Gamemode.ADVENTURE.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha",
|
||||
"Make the player type a code from a virtual map in chat after the gravity check");
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.enabled",
|
||||
"Should Sonar make the player pass a captcha?");
|
||||
verification.map.enabled = generalConfig.getBoolean("verification.checks.map-captcha.enabled", false);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.background-noise",
|
||||
"Should Sonar draw a white/light gray rectangle behind the captcha?");
|
||||
verification.map.drawBackgroundNoise = generalConfig.getBoolean("verification.checks.map-captcha" +
|
||||
".background-noise", true);
|
||||
"Make the player type a code from a virtual map in chat");
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.timing",
|
||||
"When should Sonar make the player solve a captcha?"
|
||||
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
|
||||
+ LINE_SEPARATOR + "- ALWAYS: New players will always receive a captcha"
|
||||
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only receive a captcha during an attack"
|
||||
+ LINE_SEPARATOR + "- NEVER: New players will never receive a captcha (Recommended)");
|
||||
verification.map.timing = Verification.Timing.valueOf(
|
||||
generalConfig.getString("verification.checks.map-captcha.timing",
|
||||
Verification.Timing.NEVER.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-position",
|
||||
"Should Sonar randomize the X and Y position of the captcha?");
|
||||
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position",
|
||||
true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.distortions-factor-x",
|
||||
"How much should Sonar distort characters (factor for randomization)?");
|
||||
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-x", 50), 0, 100) / 100D;
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.distortions-factor-y",
|
||||
"How much should Sonar distort characters (factor for randomization)?");
|
||||
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-y", 50), 0, 100) / 100D;
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-lines",
|
||||
"How many random lines behind the captcha should Sonar draw?");
|
||||
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines", 4);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-ovals",
|
||||
"How many random ovals behind the captcha should Sonar draw?");
|
||||
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals", 1);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-font-size",
|
||||
"Should Sonar randomize the size of the font used for rendering the captcha?");
|
||||
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size",
|
||||
true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.precompute",
|
||||
"How many answers should Sonar precompute (prepare)?");
|
||||
"How many answers should Sonar precompute (prepare)?"
|
||||
+ LINE_SEPARATOR + "This task happens asynchronously in the background;"
|
||||
+ LINE_SEPARATOR + "Players are able to join once one captcha has been prepared");
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 1000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration",
|
||||
@@ -457,6 +489,11 @@ public final class SonarConfiguration {
|
||||
"Characters (letters and numbers) that are allowed to appear in the answer to the captcha");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "123456789");
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.fonts",
|
||||
"Which font types should Sonar use for the map captcha codes?");
|
||||
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts",
|
||||
Arrays.asList(Font.DIALOG, Font.DIALOG_INPUT, Font.SERIF, Font.SANS_SERIF));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
|
||||
"Regex for validating usernames during verification");
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString(
|
||||
@@ -891,6 +928,17 @@ public final class SonarConfiguration {
|
||||
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect",
|
||||
"%prefix%<red>You have entered the wrong code. Please try again.")));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.currently-preparing",
|
||||
"Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet");
|
||||
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList(
|
||||
"verification.currently-preparing",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<yellow>Your anti-bot data has not been prepared yet.",
|
||||
"<gray>Please wait a few seconds before trying to verify again.",
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.too-many-players",
|
||||
"Disconnect message that is shown when too many players are verifying at the same time");
|
||||
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players",
|
||||
|
||||
+45
@@ -25,6 +25,7 @@ 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.dependencies.DependencyLoader;
|
||||
@@ -32,6 +33,9 @@ import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -72,6 +76,9 @@ public final class VerifiedPlayerController {
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
queryBuilder = dao.queryBuilder();
|
||||
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
|
||||
@@ -197,6 +204,44 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp.
|
||||
*/
|
||||
public void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
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 {
|
||||
final long timestamp = Instant.now()
|
||||
.minus(maximumAge, ChronoUnit.DAYS)
|
||||
.getEpochSecond() * 1000L; // convert to ms
|
||||
|
||||
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
|
||||
.lt("timestamp", new Timestamp(timestamp))
|
||||
.query();
|
||||
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @param uuid UUID associated to the IP
|
||||
|
||||
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
@@ -67,7 +66,14 @@ public final class Fallback {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
val timing = Sonar.get().getConfig().getVerification().getTiming();
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getTiming());
|
||||
}
|
||||
|
||||
public boolean shouldDoMapCaptcha() {
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
}
|
||||
|
||||
private boolean performTimingCheck(final SonarConfiguration.Verification.Timing timing) {
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
|
||||
@@ -29,13 +29,13 @@ import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"redundantSuppression"})
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
|
||||
+1
-1
@@ -224,7 +224,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
@@ -22,6 +22,7 @@ import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
@@ -32,8 +33,8 @@ import java.util.Objects;
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
|
||||
+4
-3
@@ -24,20 +24,21 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.InetAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
private final @NotNull InetAddress inetAddress;
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
|
||||
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
// Make sure other handlers also know we've disconnected
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -537,7 +537,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void captchaOrFinish() {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
|
||||
if (user.getFallback().shouldDoMapCaptcha()) {
|
||||
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
|
||||
// This should not happen, but we have to return if there is no captcha prepared
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
|
||||
return;
|
||||
}
|
||||
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
|
||||
state = State.MAP_CAPTCHA;
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
|
||||
+1
-1
@@ -235,7 +235,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x60, MINECRAFT_1_19_3, false),
|
||||
map(0x64, MINECRAFT_1_19_4, false),
|
||||
map(0x67, MINECRAFT_1_20_2, false),
|
||||
map(0x69, MINECRAFT_1_20_2, false));
|
||||
map(0x69, MINECRAFT_1_20_3, false));
|
||||
clientbound.register(GameEvent.class, GameEvent::new,
|
||||
map(0x20, MINECRAFT_1_20_3, true));
|
||||
|
||||
|
||||
+2
-5
@@ -19,7 +19,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
@@ -121,15 +121,12 @@ public class FallbackPreparer {
|
||||
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
|
||||
}
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER) {
|
||||
enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode());
|
||||
incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha());
|
||||
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Precomputing map captcha answers...");
|
||||
// Precompute captcha answers
|
||||
MapInfoPreparer.prepare();
|
||||
Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}s!", timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
+186
-93
@@ -17,29 +17,37 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
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.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static java.awt.Font.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo.DIMENSIONS;
|
||||
|
||||
@UtilityClass
|
||||
public class MapInfoPreparer {
|
||||
private final Random RANDOM = new Random();
|
||||
|
||||
private final String[] FONT_TYPES = new String[]{
|
||||
Font.DIALOG_INPUT,
|
||||
Font.DIALOG,
|
||||
Font.SANS_SERIF,
|
||||
Font.SERIF
|
||||
};
|
||||
private final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final int[] FONT_STYLES = new int[]{
|
||||
Font.PLAIN,
|
||||
Font.BOLD,
|
||||
Font.ITALIC,
|
||||
Font.ITALIC | Font.BOLD
|
||||
PLAIN,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
ITALIC | BOLD
|
||||
};
|
||||
|
||||
private final int[][] COLOR_PALETTE = new int[][]{
|
||||
@@ -49,11 +57,11 @@ public class MapInfoPreparer {
|
||||
50,
|
||||
51
|
||||
},
|
||||
new int[]{ // Black
|
||||
27,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
new int[]{ // Orange
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
},
|
||||
new int[]{ // Green
|
||||
4,
|
||||
@@ -61,6 +69,12 @@ public class MapInfoPreparer {
|
||||
29,
|
||||
30,
|
||||
},
|
||||
new int[]{ // Black
|
||||
27,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
},
|
||||
new int[]{ // Red
|
||||
16,
|
||||
17,
|
||||
@@ -70,85 +84,164 @@ public class MapInfoPreparer {
|
||||
};
|
||||
|
||||
private PreparedMapInfo[] cached;
|
||||
|
||||
public void prepare() {
|
||||
cached = new PreparedMapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()];
|
||||
|
||||
final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary();
|
||||
|
||||
for (int i = 0; i < cached.length; i++) {
|
||||
// Create image
|
||||
final BufferedImage image = new BufferedImage(PreparedMapInfo.DIMENSIONS, PreparedMapInfo.DIMENSIONS,
|
||||
BufferedImage.TYPE_3BYTE_BGR);
|
||||
final Graphics2D graphics = image.createGraphics();
|
||||
|
||||
graphics.setColor(Color.WHITE);
|
||||
|
||||
// Create random font
|
||||
final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)];
|
||||
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
|
||||
final int fontSize = 30
|
||||
+ (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 5);
|
||||
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
|
||||
graphics.setFont(answerFont);
|
||||
|
||||
// Build answer to the captcha
|
||||
final StringBuilder answerBuilder = new StringBuilder();
|
||||
for (int _i = 0; _i < 4; _i++) {
|
||||
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
|
||||
}
|
||||
final String answer = answerBuilder.toString();
|
||||
|
||||
// Calculate text position
|
||||
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
|
||||
int _x = image.getWidth() / 2 - stringWidth / 2;
|
||||
int _y = image.getHeight() / 2 + fontSize / 3;
|
||||
int randomOffsetX = 0, randomOffsetY = 3;
|
||||
final int firstPixelX = _x, firstPixelY = _y;
|
||||
|
||||
// Draw each character one by one
|
||||
final FontMetrics fontMetrics = graphics.getFontMetrics();
|
||||
for (final char c : answer.toCharArray()) {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isRandomizePositions()) {
|
||||
_x += randomOffsetX;
|
||||
_y += randomOffsetY;
|
||||
randomOffsetX = randomOffsetX < 0 ? 1 + RANDOM.nextInt(2) : -1 - RANDOM.nextInt(2);
|
||||
randomOffsetY = randomOffsetY < 0 ? 1 + RANDOM.nextInt(4) : -1 - RANDOM.nextInt(4);
|
||||
}
|
||||
|
||||
final String character = String.valueOf(c);
|
||||
graphics.drawString(character, _x, _y);
|
||||
_x += fontMetrics.stringWidth(character);
|
||||
}
|
||||
|
||||
// Select random color palette
|
||||
final int[] colorPalette = COLOR_PALETTE[i % COLOR_PALETTE.length];
|
||||
// Calculate y and height
|
||||
final int pixelY = firstPixelY - fontSize;
|
||||
final int height = firstPixelY + fontSize / 3;
|
||||
// Store image in buffer
|
||||
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
|
||||
for (int x = firstPixelX; x < _x; x++) {
|
||||
for (int y = pixelY; y < height; y++) {
|
||||
final int index = y * image.getWidth() + x;
|
||||
final int pixel = image.getRGB(x, y);
|
||||
// If the pixel has no color set, set it to white/light gray (if enabled in the config)
|
||||
if (pixel == -16777216) {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isDrawBackgroundNoise()) {
|
||||
buffer[index] = (byte) (RANDOM.nextInt(100) < 70 ? 14 : 26);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Set color of pixel to random color from the palette
|
||||
buffer[index] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)];
|
||||
}
|
||||
}
|
||||
// Cache buffer to map
|
||||
cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
|
||||
}
|
||||
}
|
||||
@Getter
|
||||
private int preparedCAPTCHAs;
|
||||
private boolean currentlyPreparing;
|
||||
|
||||
public PreparedMapInfo getRandomCaptcha() {
|
||||
return cached[RANDOM.nextInt(cached.length)];
|
||||
return cached[RANDOM.nextInt(preparedCAPTCHAs)];
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Precomputing map captcha answers...");
|
||||
|
||||
// Make sure we're not running into concurrency issues
|
||||
if (currentlyPreparing) {
|
||||
Sonar.get().getLogger().warn("Did not precompute map captcha answers as another task is running.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset all statistics
|
||||
preparedCAPTCHAs = 0;
|
||||
currentlyPreparing = true;
|
||||
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
|
||||
cached = null;
|
||||
cached = new PreparedMapInfo[config.getPrecomputeAmount()];
|
||||
|
||||
// Prepare fonts from config
|
||||
final String dictionary = config.getDictionary();
|
||||
final List<String> fonts = config.getFonts();
|
||||
if (fonts.isEmpty()) {
|
||||
Sonar.get().getLogger().warn("No fonts found, using fallback font...");
|
||||
fonts.add(DIALOG);
|
||||
}
|
||||
final String[] fontTypes = fonts.toArray(new String[0]);
|
||||
|
||||
// Prepare distortions values from config
|
||||
final double distortionsFactorX = config.getDistortionsFactorX();
|
||||
final double distortionsFactorY = config.getDistortionsFactorY();
|
||||
final double halfDistortionsFactorX = distortionsFactorX / 2D;
|
||||
final double halfDistortionsFactorY = distortionsFactorY / 2D;
|
||||
|
||||
for (int _i = 0; _i < cached.length; _i++) {
|
||||
final int currentIndex = _i;
|
||||
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
|
||||
PREPARATION_SERVICE.execute(() -> {
|
||||
// Create image
|
||||
final BufferedImage image = new BufferedImage(DIMENSIONS, DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR);
|
||||
final Graphics2D graphics = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
// Create random font
|
||||
final String fontType = fontTypes[RANDOM.nextInt(fontTypes.length)];
|
||||
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
|
||||
final int fontSize = 27
|
||||
+ (config.isRandomizeFontSize()
|
||||
? RANDOM.nextInt(6) - 2 : 5);
|
||||
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
|
||||
graphics.setFont(answerFont);
|
||||
|
||||
// Build answer to the captcha
|
||||
final StringBuilder answerBuilder = new StringBuilder();
|
||||
for (int _j = 0; _j < 5; _j++) {
|
||||
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
|
||||
}
|
||||
final String answer = answerBuilder.toString();
|
||||
|
||||
// Calculate text position
|
||||
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
|
||||
final int halfWidth = image.getWidth() / 2;
|
||||
final int halfHeight = image.getHeight() / 2;
|
||||
final int spacing = 5;
|
||||
double _x = halfWidth - stringWidth / 2f - spacing;
|
||||
double _y = halfHeight + fontSize / 3f;
|
||||
|
||||
// Draw each character one by one
|
||||
final FontRenderContext fontRenderContext = graphics.getFontRenderContext();
|
||||
for (final char c : answer.toCharArray()) {
|
||||
// Randomize x and y
|
||||
if (config.isRandomizePositions()) {
|
||||
_y += RANDOM.nextInt(8) - 4;
|
||||
}
|
||||
|
||||
// Apply random distortion
|
||||
final double distortionFactorX = RANDOM.nextDouble() * distortionsFactorX - halfDistortionsFactorX;
|
||||
final double distortionFactorY = RANDOM.nextDouble() * distortionsFactorY - halfDistortionsFactorY;
|
||||
|
||||
// Create a GlyphVector for the character
|
||||
final String character = String.valueOf(c);
|
||||
final GlyphVector glyphVector = graphics.getFont().createGlyphVector(fontRenderContext, character);
|
||||
|
||||
// Apply the distortion to the GlyphVector using AffineTransform
|
||||
final AffineTransform transform = AffineTransform.getTranslateInstance(_x, _y);
|
||||
transform.shear(distortionFactorX, distortionFactorY);
|
||||
final Shape distortedCharacter = glyphVector.getOutline();
|
||||
final Shape distorted = transform.createTransformedShape(distortedCharacter);
|
||||
|
||||
// Draw the distorted character
|
||||
graphics.fill(distorted);
|
||||
|
||||
// Update x by width
|
||||
_x += glyphVector.getVisualBounds().getWidth() + spacing;
|
||||
}
|
||||
|
||||
// TODO: Color converter
|
||||
graphics.setColor(Color.RED);
|
||||
|
||||
// Draw random lines
|
||||
for (int i = 0; i < config.getRandomLinesAmount(); i++) {
|
||||
final int startX = RANDOM.nextInt(halfWidth);
|
||||
final int startY = RANDOM.nextInt(halfHeight);
|
||||
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
|
||||
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
|
||||
|
||||
graphics.drawLine(startX, startY, endX, endY);
|
||||
}
|
||||
|
||||
// Draw random ovals
|
||||
for (int i = 0; i < config.getRandomOvalsAmount(); i++) {
|
||||
final int startX = RANDOM.nextInt(halfWidth);
|
||||
final int startY = RANDOM.nextInt(halfHeight);
|
||||
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
|
||||
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
|
||||
|
||||
graphics.drawOval(startX, startY, endX, endY);
|
||||
}
|
||||
|
||||
// Select random color palette
|
||||
final int[] colorPalette = COLOR_PALETTE[currentIndex % COLOR_PALETTE.length];
|
||||
// Select the next color palette for geometry
|
||||
final int[] nextColorPalette = COLOR_PALETTE[(currentIndex + 1) % COLOR_PALETTE.length];
|
||||
// Clear background
|
||||
Arrays.fill(buffer, (byte) 57);
|
||||
// Color every pixel individually
|
||||
final int spacingY = halfHeight - fontSize;
|
||||
for (int x = spacing; x < image.getWidth() - spacing; x++) {
|
||||
for (int y = spacingY; y < image.getHeight() - spacingY; y++) {
|
||||
final int pixel = image.getRGB(x, y);
|
||||
final int index = y * image.getWidth() + x;
|
||||
if (pixel == -16777216 && RANDOM.nextInt(100) < 97) continue;
|
||||
// Set color of pixel to random color from the palette
|
||||
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 : RANDOM.nextInt(Byte.MAX_VALUE)
|
||||
: pixel != -65536 ? colorPalette[RANDOM.nextInt(colorPalette.length)]
|
||||
: nextColorPalette[RANDOM.nextInt(nextColorPalette.length)]);
|
||||
// Write pixel color to buffer
|
||||
buffer[index] = color;
|
||||
}
|
||||
}
|
||||
// Cache buffer to map
|
||||
cached[currentIndex] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
|
||||
} finally {
|
||||
// Dispose graphics
|
||||
graphics.dispose();
|
||||
// Finished?
|
||||
if (currentIndex == cached.length - 1) {
|
||||
Sonar.get().getLogger().info("Successfully precomputed map captcha answers in {}s!", timer);
|
||||
currentlyPreparing = false;
|
||||
}
|
||||
preparedCAPTCHAs++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -34,7 +34,7 @@ import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.nbt.NBTMessageUtil.fromJson;
|
||||
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
@@ -77,7 +77,7 @@ public final class Chat implements FallbackPacket {
|
||||
public Chat(final Component component, final String message, final byte type) {
|
||||
this(component, message, type,
|
||||
false, false, null, null,
|
||||
0L, false, null, fromJson(new JsonParser().parse(message)));
|
||||
0L, false, null, serialize(new JsonParser().parse(message)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.nbt.NBTMessageUtil.fromJson;
|
||||
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
|
||||
@@ -59,7 +59,7 @@ public final class Disconnect implements FallbackPacket {
|
||||
|
||||
public static @NotNull Disconnect create(final Component component) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
final BinaryTag binaryTag = fromJson(new JsonParser().parse(serialized));
|
||||
final BinaryTag binaryTag = serialize(new JsonParser().parse(serialized));
|
||||
return new Disconnect(serialized, binaryTag);
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.utility.component;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.internal.LazilyParsedNumber;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
|
||||
@UtilityClass
|
||||
public class ComponentSerializer {
|
||||
public static BinaryTag serialize(final JsonElement json) {
|
||||
if (json instanceof JsonPrimitive) {
|
||||
final JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
|
||||
|
||||
if (jsonPrimitive.isNumber()) {
|
||||
final Number number = json.getAsNumber();
|
||||
|
||||
if (number instanceof Byte) {
|
||||
return ByteBinaryTag.byteBinaryTag((Byte) number);
|
||||
} else if (number instanceof Short) {
|
||||
return ShortBinaryTag.shortBinaryTag((Short) number);
|
||||
} else if (number instanceof Integer) {
|
||||
return IntBinaryTag.intBinaryTag((Integer) number);
|
||||
} else if (number instanceof Long) {
|
||||
return LongBinaryTag.longBinaryTag((Long) number);
|
||||
} else if (number instanceof Float) {
|
||||
return FloatBinaryTag.floatBinaryTag((Float) number);
|
||||
} else if (number instanceof Double) {
|
||||
return DoubleBinaryTag.doubleBinaryTag((Double) number);
|
||||
} else if (number instanceof LazilyParsedNumber) {
|
||||
return IntBinaryTag.intBinaryTag(number.intValue());
|
||||
}
|
||||
} else if (jsonPrimitive.isString()) {
|
||||
return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString());
|
||||
} else if (jsonPrimitive.isBoolean()) {
|
||||
return ByteBinaryTag.byteBinaryTag((byte) (jsonPrimitive.getAsBoolean() ? 1 : 0));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive);
|
||||
}
|
||||
} else if (json instanceof JsonObject) {
|
||||
final CompoundBinaryTag.Builder compound = CompoundBinaryTag.builder();
|
||||
|
||||
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
|
||||
compound.put(property.getKey(), serialize(property.getValue()));
|
||||
}
|
||||
|
||||
return compound.build();
|
||||
} else if (json instanceof JsonArray) {
|
||||
final JsonArray jsonArray = (JsonArray) json;
|
||||
|
||||
if (jsonArray.size() == 0) {
|
||||
return ListBinaryTag.empty();
|
||||
}
|
||||
|
||||
final List<BinaryTag> tagItems = new ArrayList<>(jsonArray.size());
|
||||
BinaryTagType<? extends BinaryTag> listType = null;
|
||||
|
||||
for (final JsonElement jsonEl : jsonArray) {
|
||||
final BinaryTag tag = serialize(jsonEl);
|
||||
tagItems.add(tag);
|
||||
|
||||
if (listType == null) {
|
||||
listType = tag.type();
|
||||
} else if (listType != tag.type()) {
|
||||
listType = BinaryTagTypes.COMPOUND;
|
||||
}
|
||||
}
|
||||
|
||||
switch (listType.id()) {
|
||||
case 1://BinaryTagTypes.BYTE:
|
||||
final byte[] bytes = new byte[jsonArray.size()];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (Byte) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
|
||||
return ByteArrayBinaryTag.byteArrayBinaryTag(bytes);
|
||||
case 3://BinaryTagTypes.INT:
|
||||
final int[] ints = new int[jsonArray.size()];
|
||||
for (int i = 0; i < ints.length; i++) {
|
||||
ints[i] = (Integer) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
|
||||
return IntArrayBinaryTag.intArrayBinaryTag(ints);
|
||||
case 4://BinaryTagTypes.LONG:
|
||||
final long[] longs = new long[jsonArray.size()];
|
||||
for (int i = 0; i < longs.length; i++) {
|
||||
longs[i] = (Long) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
|
||||
return LongArrayBinaryTag.longArrayBinaryTag(longs);
|
||||
case 10://BinaryTagTypes.COMPOUND:
|
||||
tagItems.replaceAll(tag -> {
|
||||
if (tag.type() == BinaryTagTypes.COMPOUND) {
|
||||
return tag;
|
||||
} else {
|
||||
return CompoundBinaryTag.builder().put("", tag).build();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
return ListBinaryTag.listBinaryTag(listType, tagItems);
|
||||
}
|
||||
return EndBinaryTag.endBinaryTag();
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.utility.nbt;
|
||||
|
||||
import com.google.gson.*;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-0e355ffd4eb0e15e2c2e628227debc9b6ccefeb167d7b9619311cdc7db2645ec
|
||||
@UtilityClass
|
||||
public class NBTMessageUtil {
|
||||
public static BinaryTag fromJson(final JsonElement json) {
|
||||
if (json instanceof JsonPrimitive) {
|
||||
JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
|
||||
if (jsonPrimitive.isNumber()) {
|
||||
Number number = json.getAsNumber();
|
||||
|
||||
if (number instanceof Byte) {
|
||||
return ByteBinaryTag.byteBinaryTag((Byte) number);
|
||||
} else if (number instanceof Short) {
|
||||
return ShortBinaryTag.shortBinaryTag((Short) number);
|
||||
} else if (number instanceof Integer) {
|
||||
return IntBinaryTag.intBinaryTag((Integer) number);
|
||||
} else if (number instanceof Long) {
|
||||
return LongBinaryTag.longBinaryTag((Long) number);
|
||||
} else if (number instanceof Float) {
|
||||
return FloatBinaryTag.floatBinaryTag((Float) number);
|
||||
} else if (number instanceof Double) {
|
||||
return DoubleBinaryTag.doubleBinaryTag((Double) number);
|
||||
}
|
||||
} else if (jsonPrimitive.isString()) {
|
||||
return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString());
|
||||
} else if (jsonPrimitive.isBoolean()) {
|
||||
return ByteBinaryTag.byteBinaryTag(jsonPrimitive.getAsBoolean() ? (byte) 1 : (byte) 0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive);
|
||||
}
|
||||
} else if (json instanceof JsonObject) {
|
||||
final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder();
|
||||
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
|
||||
builder.put(property.getKey(), fromJson(property.getValue()));
|
||||
}
|
||||
return builder.build();
|
||||
} else if (json instanceof JsonArray) {
|
||||
final JsonArray jsonArray = ((JsonArray) json);
|
||||
|
||||
if (jsonArray.size() == 0) {
|
||||
return ListBinaryTag.listBinaryTag(EndBinaryTag.endBinaryTag().type(), Collections.emptyList());
|
||||
}
|
||||
|
||||
final BinaryTagType<?> tagByteType = ByteBinaryTag.ZERO.type();
|
||||
final BinaryTagType<?> tagIntType = IntBinaryTag.intBinaryTag(0).type();
|
||||
final BinaryTagType<?> tagLongType = LongBinaryTag.longBinaryTag(0).type();
|
||||
|
||||
final BinaryTag listTag;
|
||||
final BinaryTagType<?> listType = fromJson(jsonArray.get(0)).type();
|
||||
|
||||
if (listType.equals(tagByteType)) {
|
||||
byte[] bytes = new byte[jsonArray.size()];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (Byte) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
listTag = ByteArrayBinaryTag.byteArrayBinaryTag(bytes);
|
||||
} else if (listType.equals(tagIntType)) {
|
||||
int[] ints = new int[jsonArray.size()];
|
||||
for (int i = 0; i < ints.length; i++) {
|
||||
ints[i] = (Integer) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
listTag = IntArrayBinaryTag.intArrayBinaryTag(ints);
|
||||
} else if (listType.equals(tagLongType)) {
|
||||
long[] longs = new long[jsonArray.size()];
|
||||
for (int i = 0; i < longs.length; i++) {
|
||||
longs[i] = (Long) jsonArray.get(i).getAsNumber();
|
||||
}
|
||||
listTag = LongArrayBinaryTag.longArrayBinaryTag(longs);
|
||||
} else {
|
||||
final List<BinaryTag> tagItems = new ArrayList<>(jsonArray.size());
|
||||
|
||||
for (final JsonElement jsonEl : jsonArray) {
|
||||
final BinaryTag subTag = fromJson(jsonEl);
|
||||
if (subTag.type() != listType) {
|
||||
throw new IllegalArgumentException("Cannot convert mixed JsonArray to Tag");
|
||||
}
|
||||
tagItems.add(subTag);
|
||||
}
|
||||
listTag = ListBinaryTag.listBinaryTag(listType, tagItems);
|
||||
}
|
||||
return listTag;
|
||||
} else if (json instanceof JsonNull) {
|
||||
return EndBinaryTag.endBinaryTag();
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown JSON element: " + json);
|
||||
}
|
||||
}
|
||||
@@ -24,25 +24,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@UtilityClass
|
||||
public class VarIntUtil {
|
||||
private static final int[] EXACT_BYTE_LENGTHS = new int[33];
|
||||
|
||||
static {
|
||||
for (int i = 0; i <= 32; ++i) {
|
||||
EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
|
||||
}
|
||||
EXACT_BYTE_LENGTHS[32] = 1;
|
||||
}
|
||||
|
||||
public static int varIntBytes(final int value) {
|
||||
return EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
|
||||
}
|
||||
|
||||
public static void write21BitVarInt(final @NotNull ByteBuf byteBuf, final int value) {
|
||||
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
||||
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||
byteBuf.writeMedium(w);
|
||||
}
|
||||
|
||||
public static int readVarInt(final ByteBuf byteBuf) {
|
||||
int read = readVarIntSafely(byteBuf);
|
||||
if (read == Integer.MIN_VALUE) {
|
||||
|
||||
+2
-3
@@ -228,7 +228,7 @@ public final class FallbackListener {
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
@@ -277,8 +277,7 @@ public final class FallbackListener {
|
||||
final FallbackUserWrapper user = new FallbackUserWrapper(
|
||||
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
|
||||
mcConnection.getChannel().pipeline(), inetAddress,
|
||||
ProtocolVersion.fromId(protocolId)
|
||||
);
|
||||
ProtocolVersion.fromId(protocolId));
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
|
||||
Reference in new issue
Block a user