mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 17:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59731a93f1 | ||
|
|
ace55fbe7b | ||
|
|
696d3c5c81 | ||
|
|
a1e4ea3f5b | ||
|
|
6b054bd56c | ||
|
|
c8e2142dd9 | ||
|
|
1cf7ef6994 | ||
|
|
af8aa103bf | ||
|
|
d1f2c15a8f | ||
|
|
5a68c4d557 |
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.1.6
|
||||
version=2.1.7
|
||||
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
|
||||
@@ -197,7 +197,6 @@ public final class SonarConfiguration {
|
||||
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");
|
||||
@@ -415,7 +414,6 @@ public final class SonarConfiguration {
|
||||
private Pattern validNameRegex;
|
||||
private Pattern validLocaleRegex;
|
||||
|
||||
private int maxLoginPackets;
|
||||
private int readTimeout;
|
||||
private int writeTimeout;
|
||||
private int reconnectDelay;
|
||||
|
||||
+19
-76
@@ -28,7 +28,6 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
@@ -38,15 +37,18 @@ 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.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 {
|
||||
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
|
||||
@Getter
|
||||
private final Set<String> cache = Collections.synchronizedSet(new HashSet<>());
|
||||
private @Nullable ConnectionSource connectionSource;
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@@ -97,6 +99,7 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException ignored) {
|
||||
ignored.printStackTrace();
|
||||
/*
|
||||
* This is caused by a duplicate index;
|
||||
* I know this isn't the best method of handling it,
|
||||
@@ -114,13 +117,13 @@ public final class VerifiedPlayerController {
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(database.getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
dao.queryForAll().forEach(verifiedPlayer -> cache.add(verifiedPlayer.getFingerprint()));
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error initializing database: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
});
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error setting up database: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +136,7 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
connectionSource.close();
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Error closing database: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,10 +166,10 @@ public final class VerifiedPlayerController {
|
||||
* First, remove the player from the local cache and then,
|
||||
* secondly, asynchronously add the player to the database.
|
||||
*
|
||||
* @param inetAddress InetAddress of the VerifiedPlayer model
|
||||
* @param fingerprint Fingerprint of the verified player
|
||||
*/
|
||||
public void remove(final @NotNull String inetAddress) {
|
||||
_remove(inetAddress);
|
||||
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) {
|
||||
@@ -182,7 +185,7 @@ public final class VerifiedPlayerController {
|
||||
|
||||
try {
|
||||
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
|
||||
.eq("ip_address", inetAddress)
|
||||
.eq("fingerprint", fingerprint)
|
||||
.query();
|
||||
|
||||
if (verifiedPlayer != null) {
|
||||
@@ -191,20 +194,11 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Locally remove the object from the cache
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
private void _remove(final @NotNull String inetAddress) {
|
||||
cache.remove(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* First, cache the player locally and then,
|
||||
* secondly, asynchronously add the player to the database.
|
||||
@@ -212,7 +206,7 @@ public final class VerifiedPlayerController {
|
||||
* @param player VerifiedPlayer model
|
||||
*/
|
||||
public void add(final @NotNull VerifiedPlayer player) {
|
||||
_add(player);
|
||||
cache.add(player.getFingerprint());
|
||||
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
@@ -229,21 +223,11 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
dao.createIfNotExists(player);
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Locally cache the object
|
||||
*
|
||||
* @param player VerifiedPlayer model
|
||||
*/
|
||||
private void _add(final @NotNull VerifiedPlayer player) {
|
||||
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
|
||||
.add(player.getPlayerUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of verified IP addresses
|
||||
*
|
||||
@@ -253,26 +237,6 @@ public final class VerifiedPlayerController {
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of verified IP addresses
|
||||
* and their respective collection of UUIDs
|
||||
*
|
||||
* @return Exact size of all total verified players
|
||||
*/
|
||||
@Deprecated
|
||||
public synchronized int exactSize() {
|
||||
return cache.values().stream()
|
||||
.mapToInt(Collection::size)
|
||||
.sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link java.util.Collection} of UUIDs associated with an IP address
|
||||
*/
|
||||
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
|
||||
return cache.getOrDefault(inetAddress, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the local cache, and, if the database type is set,
|
||||
* remove the table from the database.
|
||||
@@ -285,29 +249,8 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
dao.deleteBuilder().delete();
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @param uuid UUID associated to the IP
|
||||
* @return Whether the local cache contains the IP and UUID
|
||||
*/
|
||||
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
|
||||
final Collection<UUID> got = cache.get(inetAddress);
|
||||
if (got != null) {
|
||||
return got.contains(uuid);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @return Whether the local cache contains the IP
|
||||
*/
|
||||
public boolean has(final @NotNull String inetAddress) {
|
||||
return cache.containsKey(inetAddress);
|
||||
}
|
||||
}
|
||||
@@ -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.getHostAddress(), 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
-5
@@ -22,15 +22,12 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.UUID;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final UUID offlineUuid;
|
||||
private final FallbackUser user;
|
||||
private final long timeTakenToVerify;
|
||||
private final SystemTimer loginTimer;
|
||||
}
|
||||
@@ -40,7 +40,9 @@ public final class 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);
|
||||
// TODO: Is there a way to improve this?
|
||||
// (E.g. no map needed without concurrency issues)
|
||||
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(256);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
@Setter
|
||||
private Cache<String, Integer> blacklist;
|
||||
|
||||
@@ -26,7 +26,6 @@ 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
|
||||
@@ -45,15 +44,14 @@ public interface FallbackUser {
|
||||
SystemTimer getLoginTimer();
|
||||
|
||||
@NotNull
|
||||
UUID getOfflineUuid();
|
||||
String getFingerprint();
|
||||
|
||||
boolean isReceivedClientSettings();
|
||||
@NotNull
|
||||
String getUsername();
|
||||
|
||||
void setReceivedClientSettings(final boolean receivedClientSettings);
|
||||
boolean isForceCaptcha();
|
||||
|
||||
boolean isReceivedPluginMessage();
|
||||
|
||||
void setReceivedPluginMessage(final boolean receivedPluginMessage);
|
||||
void setForceCaptcha(final boolean isForceCaptcha);
|
||||
|
||||
boolean isGeyser();
|
||||
|
||||
@@ -65,14 +63,6 @@ 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 offlineUuid UUID of the player
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
*
|
||||
@@ -97,16 +87,4 @@ public interface FallbackUser {
|
||||
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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
-11
@@ -41,7 +41,7 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
|
||||
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 the rest
|
||||
// Make sure to let the server handle this
|
||||
ctx.fireChannelActive();
|
||||
// Add the packet handler pipeline
|
||||
sonarPipelineInjector.accept(ctx.pipeline());
|
||||
@@ -49,26 +49,42 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
@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 connected players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
|
||||
// Remove the IP address from the queue
|
||||
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
|
||||
// Remove this account from the online players or decrement the number of accounts with the same IP
|
||||
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
|
||||
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;
|
||||
});
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
// We can override the default exceptionCaught method since this handler
|
||||
// will run before the connection knows that there has been an error.
|
||||
/*
|
||||
* 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 @NotNull Throwable cause) throws Exception {
|
||||
// Close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
+41
-51
@@ -23,8 +23,8 @@ 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.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
@@ -33,15 +33,12 @@ import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
@@ -67,9 +64,7 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
|
||||
if (protocolVersion != null) {
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
// Hook the traffic listener
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -92,62 +87,61 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
|
||||
}
|
||||
// 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()) {
|
||||
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username and IP address
|
||||
this.username = username;
|
||||
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
|
||||
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
|
||||
final String hostAddress = socketAddress.getAddress().getHostAddress();
|
||||
|
||||
final InetAddress inetAddress = socketAddress.getAddress();
|
||||
channel.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(inboundHandler.getInetAddress())) {
|
||||
if (Sonar.get().getFallback().getConnected().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying);
|
||||
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())) {
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted);
|
||||
return;
|
||||
}
|
||||
|
||||
final String hostAddress = inetAddress.getHostAddress();
|
||||
// Check if the player failed the verification too many times
|
||||
final int score = Sonar.get().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
|
||||
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
if (score >= limit) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted);
|
||||
return;
|
||||
if (limit > 0) {
|
||||
final int score = Sonar.get().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
|
||||
if (score >= limit) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
|
||||
initialLogin(ctx, inetAddress, loginPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections if configured
|
||||
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
|
||||
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
|
||||
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
|
||||
initialLogin(ctx, inetAddress, loginPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUuidString = "OfflinePlayer:" + username;
|
||||
final UUID offlineUuid = UUID.nameUUIDFromBytes(offlineUuidString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(hostAddress, offlineUuid)) {
|
||||
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
|
||||
initialLogin(ctx, inetAddress, loginPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!Sonar.get().getFallback().getRatelimiter().attempt(inboundHandler.getInetAddress())) {
|
||||
if (!Sonar.get().getFallback().getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast);
|
||||
return;
|
||||
}
|
||||
@@ -156,9 +150,9 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
|
||||
rewriteProtocol(ctx, channelRemovalListener);
|
||||
|
||||
// Queue the connection for further processing
|
||||
Sonar.get().getFallback().getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
|
||||
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (__, action) -> {
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (runnable != null) {
|
||||
if (action != null) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued);
|
||||
// Remove other instances of this IP address from the queue
|
||||
return null;
|
||||
@@ -167,17 +161,13 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
|
||||
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()) {
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(username).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUser user = new FallbackUserWrapper(
|
||||
channel, inboundHandler.getInetAddress(), protocolVersion, offlineUuid, geyser);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(username, offlineUuid);
|
||||
// Create an instance for the user and let the verification handler take over the channel
|
||||
new FallbackUserWrapper(channel, inetAddress, protocolVersion, username, fingerprint, geyser);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -216,23 +206,23 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
|
||||
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull Runnable loginPacket) throws Exception {
|
||||
// Increment the number of accounts with the same IP
|
||||
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
|
||||
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Skip the maximum online per IP check if it's disabled in the configuration
|
||||
if (maxOnlinePerIp > 0) {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int onlinePerIp = Sonar.get().getFallback().getOnline().getOrDefault(inetAddress, 0);
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
|
||||
return;
|
||||
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(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
|
||||
return count + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Let the server know about the login packet
|
||||
loginPacket.run();
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+23
-103
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -26,8 +25,6 @@ import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
@@ -37,20 +34,18 @@ 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.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.session.FallbackLoginSessionHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.verification.FallbackPreJoinHandler;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.loginSuccess;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
|
||||
@@ -59,36 +54,28 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final UUID offlineUuid;
|
||||
@Setter
|
||||
private boolean receivedClientSettings;
|
||||
@Setter
|
||||
private boolean receivedPluginMessage;
|
||||
private final String fingerprint;
|
||||
private final String username;
|
||||
private final boolean geyser;
|
||||
private final SystemTimer loginTimer = new SystemTimer();
|
||||
private final SystemTimer loginTimer;
|
||||
@Setter
|
||||
private boolean forceCaptcha;
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull UUID offlineUuid,
|
||||
final @NotNull String username,
|
||||
final @NotNull String fingerprint,
|
||||
final boolean geyser) {
|
||||
this.channel = channel;
|
||||
this.pipeline = channel.pipeline();
|
||||
this.inetAddress = inetAddress;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.offlineUuid = offlineUuid;
|
||||
this.username = username;
|
||||
this.fingerprint = fingerprint;
|
||||
this.geyser = geyser;
|
||||
}
|
||||
this.loginTimer = new SystemTimer();
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
|
||||
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
|
||||
GlobalSonarStatistics.totalAttemptedVerifications++;
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()
|
||||
@@ -112,7 +99,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
}
|
||||
|
||||
// Mark the player as connected by caching them in a map of verifying players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (_k, _v) -> (byte) 0);
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> (byte) 0);
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
@@ -120,17 +107,17 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
pipeline.addLast(FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(FallbackPreparer.loginSuccess);
|
||||
write(loginSuccess);
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
final FallbackPacketDecoder newDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
pipeline.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
|
||||
pipeline.addLast(FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
pipeline.addLast(FALLBACK_PACKET_DECODER, newDecoder);
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username));
|
||||
newDecoder.setListener(new FallbackPreJoinHandler(this));
|
||||
|
||||
// Make sure to catch all exceptions during the verification
|
||||
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
|
||||
@@ -138,76 +125,9 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(final @NotNull String reason) {
|
||||
GlobalSonarStatistics.totalFailedVerifications++;
|
||||
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("<reason>", reason));
|
||||
}
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Use a label, so we can easily add more code beneath this method in the future
|
||||
blacklist: {
|
||||
// Check if the player has too many failed attempts
|
||||
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
if (limit <= 0) break blacklist;
|
||||
|
||||
final String hostAddress = inetAddress.getHostAddress();
|
||||
final int score = Sonar.get().getFallback().getBlacklist().get(hostAddress, __ -> 0);
|
||||
final int newScore = score + 1;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(hostAddress, newScore);
|
||||
|
||||
// The player hasn't been blacklisted yet, so skip this iteration
|
||||
if (newScore < limit) break blacklist;
|
||||
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an exception to avoid further code execution
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel Channel to close
|
||||
* @param protocolVersion Protocol version
|
||||
* @param msg Packet to close the channel with
|
||||
*/
|
||||
public static void closeWith(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull Object msg) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
|
||||
channel.eventLoop().execute(() -> {
|
||||
channel.config().setAutoRead(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
|
||||
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
|
||||
}
|
||||
}
|
||||
+12
-5
@@ -30,6 +30,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
|
||||
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.fallback.protocol.FallbackPreparer.maxTotalPacketsSent;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
|
||||
@@ -38,6 +39,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
@Setter
|
||||
private FallbackPacketListener listener;
|
||||
private int totalPacketsSent;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
@@ -62,6 +64,11 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow the player to spam packets to overload netty
|
||||
if (++totalPacketsSent > maxTotalPacketsSent) {
|
||||
throw 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 FallbackPacket packet = registry.createPacket(packetId);
|
||||
@@ -102,14 +109,14 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int packetSize = byteBuf.readableBytes();
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
|
||||
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
|
||||
throw DEBUG ? new DecoderException("Packet too large") : QuietDecoderException.INSTANCE;
|
||||
if (expectedMaxLen != -1 && packetSize > expectedMaxLen) {
|
||||
throw DEBUG ? new DecoderException("Packet too large: " + packetSize) : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
|
||||
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
|
||||
throw DEBUG ? new DecoderException("Packet too small") : QuietDecoderException.INSTANCE;
|
||||
if (expectedMinLen != -1 && packetSize < expectedMinLen) {
|
||||
throw DEBUG ? new DecoderException("Packet too small: " + packetSize) : QuietDecoderException.INSTANCE;
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -363,6 +363,25 @@ public enum FallbackPacketRegistry {
|
||||
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));
|
||||
|
||||
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, false),
|
||||
|
||||
+75
-73
@@ -18,6 +18,8 @@
|
||||
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 xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -40,41 +42,51 @@ import java.util.UUID;
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
private final Random RANDOM = new Random();
|
||||
// LoginSuccess
|
||||
public FallbackPacket loginSuccess;
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x00, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x02, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x06, 0f, 0f));
|
||||
// Chunks
|
||||
|
||||
public static final int PLAYER_ENTITY_ID = RANDOM.nextInt(50);
|
||||
public static final int VEHICLE_BOAT_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
|
||||
public static final int VEHICLE_MINECART_ENTITY_ID = VEHICLE_BOAT_ENTITY_ID + 1 + RANDOM.nextInt(10);
|
||||
|
||||
public static final CompoundBinaryTag MAP_ITEM_NBT = CompoundBinaryTag.builder()
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
|
||||
.build();
|
||||
|
||||
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 DEFAULT_Y_COLLIDE_POSITION = 1 + RANDOM.nextInt(255); // 255 is the maximum Y position
|
||||
public final int IN_AIR_Y_POSITION = 1000 + RANDOM.nextInt(338); // High altitude (randomized)
|
||||
public final int IN_VOID_Y_POSITION = -(120 + RANDOM.nextInt(120)); // Low altitude (randomized)
|
||||
|
||||
public final int FIRST_TELEPORT_ID = RANDOM.nextInt();
|
||||
public final int SECOND_TELEPORT_ID = RANDOM.nextInt();
|
||||
|
||||
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, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, 0, false));
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket(); // No snapshot needed because it's empty either way
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
|
||||
public final FallbackPacket REGISTRY_SYNC_LEGACY = 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);
|
||||
// Keep Alive
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
|
||||
// Game Event (1.20.3+)
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new FallbackPacketSnapshot(new GameEventPacket(13, 0));
|
||||
// Chat
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
|
||||
public final static FallbackPacket CACHED_HELD_ITEM_RESET = new SetHeldItemPacket(~4);
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlivePacket(RANDOM.nextInt());
|
||||
|
||||
public static FallbackPacket loginSuccess;
|
||||
public FallbackPacket enterCodeMessage;
|
||||
public FallbackPacket incorrectCaptcha;
|
||||
// JoinGame
|
||||
public static final int PLAYER_ENTITY_ID = RANDOM.nextInt(50);
|
||||
public FallbackPacket joinGame;
|
||||
// Update Section Blocks
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
// Default Spawn Position
|
||||
public static FallbackPacket joinGame;
|
||||
public FallbackPacket defaultSpawnPosition;
|
||||
// Spawn Position
|
||||
public final int TELEPORT_ID = RANDOM.nextInt();
|
||||
public FallbackPacket spawnPosition;
|
||||
// Transfer packet
|
||||
public FallbackPacket fallStartPosition;
|
||||
public FallbackPacket fallStartPositionLegacy;
|
||||
public static FallbackPacket transferToOrigin;
|
||||
|
||||
// Disconnect messages
|
||||
public FallbackPacket blacklisted;
|
||||
public FallbackPacket alreadyQueued;
|
||||
public FallbackPacket alreadyVerifying;
|
||||
@@ -82,36 +94,16 @@ public class FallbackPreparer {
|
||||
public FallbackPacket protocolBlacklisted;
|
||||
public FallbackPacket invalidUsername;
|
||||
public FallbackPacket tooManyOnlinePerIP;
|
||||
|
||||
// Vehicle
|
||||
public FallbackPacket removeBoatEntities;
|
||||
public FallbackPacket removeMinecartEntities;
|
||||
public FallbackPacket removeBoat;
|
||||
public FallbackPacket teleportMinecart;
|
||||
public FallbackPacket spawnBoatEntity;
|
||||
public FallbackPacket spawnMinecartEntity;
|
||||
public FallbackPacket setBoatPassengers;
|
||||
public FallbackPacket setMinecartPassengers;
|
||||
public final int VEHICLE_BOAT_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
|
||||
public final int VEHICLE_MINECART_ENTITY_ID = VEHICLE_BOAT_ENTITY_ID + 1 + RANDOM.nextInt(10);
|
||||
|
||||
// Collisions
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
|
||||
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
|
||||
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 155 + RANDOM.nextInt(101); // 255 is the maximum Y position
|
||||
public final int IN_AIR_Y_POSITION = 1337;
|
||||
|
||||
// CAPTCHA position
|
||||
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, false));
|
||||
|
||||
// Platform
|
||||
public BlockType blockType;
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double maxFallDistance;
|
||||
|
||||
// XP packets
|
||||
public FallbackPacket[] xpCountdown;
|
||||
|
||||
public static int maxMovementTick, dynamicSpawnYPosition, maxTotalPacketsSent;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public void prepare() {
|
||||
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
|
||||
@@ -131,41 +123,44 @@ public class FallbackPreparer {
|
||||
|
||||
// Prepare the gravity check
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
|
||||
maxFallDistance = 1;
|
||||
|
||||
double motionY = -0.08 * 0.98f;
|
||||
double motionY = 0, fallDistance = 1;
|
||||
|
||||
for (int i = 0; i < maxMovementTick; i++) {
|
||||
motionY = (motionY - 0.08) * 0.98f;
|
||||
maxFallDistance += Math.abs(motionY);
|
||||
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 = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(fallDistance);
|
||||
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
|
||||
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));
|
||||
fallStartPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition - IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
|
||||
0, -90, SECOND_TELEPORT_ID, 1 << 1, false));
|
||||
fallStartPositionLegacy = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0, -90, TELEPORT_ID, false));
|
||||
0, -90, 0, 0, false));
|
||||
|
||||
// Prepare collision platform positions
|
||||
blockType = BlockType.valueOf(Sonar.get().getConfig().getGeneralConfig().getString(
|
||||
"verification.checks.collision.collision-block-type").toUpperCase());
|
||||
final BlockUpdate[] changedBlocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
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 + (BLOCKS_PER_ROW / 2),
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0);
|
||||
changedBlocks[index++] = new BlockUpdate(position, blockType);
|
||||
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, DEFAULT_Y_COLLIDE_POSITION, z + length, 0, 0);
|
||||
blocks[index++] = new BlockUpdate(position, POSSIBLE_BLOCK_TYPES[i]);
|
||||
}
|
||||
}
|
||||
BLOCKS_PACKETS[i] = new UpdateSectionBlocksPacket(0, 0, blocks);
|
||||
}
|
||||
|
||||
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, changedBlocks));
|
||||
|
||||
// Prepare disconnect packets during login
|
||||
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
|
||||
alreadyVerifying = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true));
|
||||
@@ -185,8 +180,9 @@ public class FallbackPreparer {
|
||||
}
|
||||
|
||||
// Prepare packets for the vehicle check
|
||||
removeBoatEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_BOAT_ENTITY_ID));
|
||||
removeMinecartEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_MINECART_ENTITY_ID));
|
||||
removeBoat = new RemoveEntitiesPacket(VEHICLE_BOAT_ENTITY_ID);
|
||||
teleportMinecart = new TeleportEntityPacket(
|
||||
VEHICLE_MINECART_ENTITY_ID, SPAWN_X_POSITION, IN_VOID_Y_POSITION, SPAWN_Z_POSITION, false);
|
||||
spawnBoatEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
|
||||
VEHICLE_BOAT_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
|
||||
spawnMinecartEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
|
||||
@@ -232,5 +228,11 @@ public class FallbackPreparer {
|
||||
xpCountdown = null;
|
||||
Sonar.get().getFallback().setCaptchaGenerator(null);
|
||||
}
|
||||
|
||||
maxTotalPacketsSent = maxMovementTick
|
||||
+ (xpCountdown == null ? 0 : xpCountdown.length) * 3
|
||||
+ Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets() * 4
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries()
|
||||
+ 150 /* some arbitrary leeway */;
|
||||
}
|
||||
}
|
||||
+244
-175
@@ -24,6 +24,9 @@ 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
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum BlockType {
|
||||
@@ -31,188 +34,254 @@ public enum BlockType {
|
||||
// - 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),
|
||||
//STONE(protocolVersion -> 1, 1),
|
||||
ENCHANTMENT_TABLE(protocolVersion -> {
|
||||
switch (protocolVersion) {
|
||||
// 1.13-1.13.1
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 4612;
|
||||
// 1.13.2-1.14.3
|
||||
case MINECRAFT_1_13_2:
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
return 4613;
|
||||
// 1.14.4-1.15.2
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 5116;
|
||||
// 1.16-1.16.1
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
return 5132;
|
||||
// 1.16.2-1.16.4
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 5136;
|
||||
// 1.17-1.18.2
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 5333;
|
||||
// 1.19-1.19.1
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 5719;
|
||||
// 1.19.3
|
||||
case MINECRAFT_1_19_3:
|
||||
return 7159;
|
||||
// 1.19.4
|
||||
case MINECRAFT_1_19_4:
|
||||
return 7385;
|
||||
// 1.20-1.21
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 7389;
|
||||
// 1.7.2-1.12.2
|
||||
default:
|
||||
return 116;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 116;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 4612;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 4613;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 5116;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 5132;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 5136;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 5333;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 5719;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 7159;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 7385;
|
||||
}
|
||||
return 7389;
|
||||
}, 0.75f),
|
||||
TRAPDOOR(protocolVersion -> {
|
||||
switch (protocolVersion) {
|
||||
// 1.7.2-1.7.6
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
|
||||
return 96;
|
||||
// 1.13-1.13.1
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 6509;
|
||||
// 1.13.2-1.14.3
|
||||
case MINECRAFT_1_13_2:
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
return 6510;
|
||||
// 1.14.4-1.15.2
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 7016;
|
||||
// 1.16-1.16.1
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
return 7552;
|
||||
// 1.16.2-1.16.4
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 7556;
|
||||
// 1.17-1.18.2
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7802;
|
||||
// 1.19-1.19.1
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 8293;
|
||||
// 1.19.3
|
||||
case MINECRAFT_1_19_3:
|
||||
return 9937;
|
||||
// 1.19.4
|
||||
case MINECRAFT_1_19_4:
|
||||
return 10269;
|
||||
// 1.20
|
||||
case MINECRAFT_1_20:
|
||||
return 10273;
|
||||
// 1.20.2-1.21
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10414;
|
||||
// 1.8-1.12.2
|
||||
default:
|
||||
return 167;
|
||||
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_7_6) <= 0) {
|
||||
return 96;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 167;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 6509;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 6510;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 7016;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 7552;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 7556;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 7802;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 8293;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 9937;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 10269;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
|
||||
return 10273;
|
||||
}
|
||||
return 10414;
|
||||
}, 0.1875),
|
||||
BARRIER(protocolVersion -> {
|
||||
switch (protocolVersion) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
|
||||
return 20;
|
||||
// 1.13-1.13.1
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 6493;
|
||||
// 1.13.2
|
||||
case MINECRAFT_1_13_2:
|
||||
return 6494;
|
||||
// 1.14-1.15.2
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 7000;
|
||||
// 1.16-1.16.1
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
return 7536;
|
||||
// 1.16.2-1.16.4
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 7540;
|
||||
// 1.17-1.18.2
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7754;
|
||||
// 1.19-1.19.1
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 8245;
|
||||
// 1.19.3
|
||||
case MINECRAFT_1_19_3:
|
||||
return 9889;
|
||||
// 1.19.4
|
||||
case MINECRAFT_1_19_4:
|
||||
return 10221;
|
||||
// 1.20
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
// 1.20.2-1.21
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10366;
|
||||
// 1.8-1.12.2
|
||||
default:
|
||||
return 166;
|
||||
END_PORTAL_FRAME(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 120;
|
||||
}
|
||||
}, 1);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 4633;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 4634;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 5137;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 5153;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 5157;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 5358;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 5744;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 7184;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 7410;
|
||||
}
|
||||
return 7414;
|
||||
}, 0.8125),
|
||||
DAYLIGHT_SENSOR(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 151;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 5651;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 5652;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 6158;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 6694;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 6698;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 6916;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 7327;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 8811;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 9063;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
|
||||
return 9067;
|
||||
}
|
||||
return 9207;
|
||||
}, 0.375),
|
||||
COBBLESTONE_WALL(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 139;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 5196;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 5197;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 5700;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 5660;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 5664;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 5866;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 6252;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 7692;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 7918;
|
||||
}
|
||||
return 7922;
|
||||
}, 1.5),
|
||||
STONE_SLABS(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 44;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 7296;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 7297;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 7809;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 8345;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 8349;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 8595;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 9092;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 10748;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 11086;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
|
||||
return 11090;
|
||||
}
|
||||
return 11231;
|
||||
}, 0.5),
|
||||
WHITE_CARPET(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 171;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 6823;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 6824;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 7330;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return 7866;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 7870;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 8116;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 8607;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 10251;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 10583;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
|
||||
return 10587;
|
||||
}
|
||||
return 10728;
|
||||
}, 0.0625);
|
||||
|
||||
private final Function<ProtocolVersion, Integer> idFunction;
|
||||
@Getter
|
||||
|
||||
+3
-3
@@ -25,12 +25,12 @@ import org.jetbrains.annotations.NotNull;
|
||||
@Getter
|
||||
public final class BlockUpdate {
|
||||
private final @NotNull BlockPosition position;
|
||||
private final BlockType type;
|
||||
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);
|
||||
|
||||
+36
-56
@@ -23,7 +23,9 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
// TODO: load mappings from a separate file
|
||||
@RequiredArgsConstructor
|
||||
public enum ItemType {
|
||||
// Useful resources:
|
||||
@@ -31,62 +33,40 @@ public enum ItemType {
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
|
||||
FILLED_MAP(protocolVersion -> {
|
||||
switch (protocolVersion) {
|
||||
// 1.13-1.13.1
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 608;
|
||||
// 1.13.2
|
||||
case MINECRAFT_1_13_2:
|
||||
return 613;
|
||||
// 1.14-1.15.2
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 671;
|
||||
// 1.16-1.16.4
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 733;
|
||||
// 1.17-1.18.2
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 847;
|
||||
// 1.19-1.19.1
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 886;
|
||||
// 1.19.3
|
||||
case MINECRAFT_1_19_3:
|
||||
return 914;
|
||||
// 1.19.4
|
||||
case MINECRAFT_1_19_4:
|
||||
return 937;
|
||||
// 1.20-1.20.2
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
return 941;
|
||||
// 1.20.3
|
||||
case MINECRAFT_1_20_3:
|
||||
return 979;
|
||||
// 1.20.5-1.21
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 982;
|
||||
default:
|
||||
// 1.7-1.12.2
|
||||
return 358;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 358;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
|
||||
return 608;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 613;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 671;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 733;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 847;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 886;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 914;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return 937;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) <= 0) {
|
||||
return 941;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
|
||||
return 979;
|
||||
}
|
||||
return 982;
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> idFunction;
|
||||
|
||||
+48
-90
@@ -23,106 +23,64 @@ 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
|
||||
@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 -> {
|
||||
switch (protocolVersion) {
|
||||
// 1.14-1.14.4
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
return 5;
|
||||
// 1.15-1.16.4
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 6;
|
||||
// 1.17-1.18.2
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7;
|
||||
// 1.19-1.19.3
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
case MINECRAFT_1_19_3:
|
||||
return 8;
|
||||
// 1.19.4-1.20.3
|
||||
case MINECRAFT_1_19_4:
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
return 9;
|
||||
// 1.20.5-1.21
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10;
|
||||
// 1.7-1.13.2
|
||||
default:
|
||||
return 1;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 1;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
|
||||
return 5;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 6;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 7;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 8;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
|
||||
return 9;
|
||||
}
|
||||
return 10;
|
||||
}),
|
||||
MINECART(protocolVersion -> {
|
||||
switch (protocolVersion) {
|
||||
// 1.14-1.14.4
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
return 41;
|
||||
// 1.15-1.15.2
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 42;
|
||||
// 1.16-1.16.4
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 45;
|
||||
// 1.17-1.18.2
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 50;
|
||||
// 1.19-1.19.1
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 53;
|
||||
// 1.19.3
|
||||
case MINECRAFT_1_19_3:
|
||||
return 54;
|
||||
// 1.19.4-1.20.2
|
||||
case MINECRAFT_1_19_4:
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
return 64;
|
||||
// 1.20.3
|
||||
case MINECRAFT_1_20_3:
|
||||
return 65;
|
||||
// 1.20.5-1.21
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 69;
|
||||
// 1.7-1.13.2
|
||||
default:
|
||||
return 10;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 10;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
|
||||
return 41;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 42;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 45;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 50;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 53;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 54;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) <= 0) {
|
||||
return 64;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
|
||||
return 65;
|
||||
}
|
||||
return 69;
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
|
||||
+4
@@ -52,6 +52,10 @@ public final class LoginSuccessPacket implements FallbackPacket {
|
||||
writeString(byteBuf, FastUuidSansHyphens.toString(uuid));
|
||||
}
|
||||
|
||||
// Cap the length of the username to 16 characters
|
||||
if (username.length() > 16) {
|
||||
username = username.substring(0, 16);
|
||||
}
|
||||
writeString(byteBuf, username);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
|
||||
+2
-4
@@ -18,10 +18,7 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@@ -33,6 +30,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString(of = {"entityId", "hand"})
|
||||
public final class AnimationPacket implements FallbackPacket {
|
||||
private int entityId = -1;
|
||||
private int hand = MAIN_HAND;
|
||||
|
||||
+5
-5
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ChunkDataPacket implements FallbackPacket {
|
||||
private int x, z;
|
||||
private int sectionX, sectionZ;
|
||||
|
||||
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
|
||||
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
|
||||
@@ -67,8 +67,8 @@ public final class ChunkDataPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(z);
|
||||
byteBuf.writeInt(sectionX);
|
||||
byteBuf.writeInt(sectionZ);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
|
||||
@@ -113,13 +113,13 @@ public final class ChunkDataPacket implements FallbackPacket {
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
|
||||
writeByteArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
|
||||
} else {
|
||||
byteBuf.writeInt(0); // compressed size
|
||||
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
|
||||
}
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
|
||||
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
|
||||
writeByteArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
|
||||
writeVarInt(byteBuf, 0); // 1.15 - 1.17.1
|
||||
} else {
|
||||
|
||||
+4
-1
@@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@@ -43,7 +45,8 @@ public final class PlayerInputPacket implements FallbackPacket {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
sideways = byteBuf.readFloat();
|
||||
forward = byteBuf.readFloat();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
jump = byteBuf.readBoolean();
|
||||
unmount = byteBuf.readBoolean();
|
||||
} else {
|
||||
|
||||
+7
-10
@@ -23,10 +23,10 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.IntBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@@ -36,16 +36,13 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
private int slot, count, itemId;
|
||||
private int windowId, slot, count;
|
||||
private ItemType itemType;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(0); // windowId
|
||||
byteBuf.writeByte(windowId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
@@ -62,9 +59,9 @@ public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
|
||||
byteBuf.writeShort(itemId);
|
||||
byteBuf.writeShort(itemType.getId(protocolVersion));
|
||||
} else {
|
||||
writeVarInt(byteBuf, itemId);
|
||||
writeVarInt(byteBuf, itemType.getId(protocolVersion));
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
@@ -86,7 +83,7 @@ public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
writeCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else { // 1.20.5
|
||||
} else { // 1.20.5+
|
||||
// TODO: find a way to improve this
|
||||
// component
|
||||
writeVarInt(byteBuf, 1); // component count to add
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@@ -40,7 +41,7 @@ public final class SetExperiencePacket implements FallbackPacket {
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeFloat(experienceBar);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(level);
|
||||
byteBuf.writeShort(totalExperience);
|
||||
} else {
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@@ -39,7 +40,7 @@ public final class SetPassengersPacket implements FallbackPacket {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
// You can find this in the EntityAttach packet,
|
||||
// which was later replaced by SetPassengers in 1.9+
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) < 0) {
|
||||
byteBuf.writeInt(passengerId);
|
||||
byteBuf.writeInt(entityId);
|
||||
byteBuf.writeByte(0); // leash
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
public final class SetPlayerPositionRotationPacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private float yaw, pitch;
|
||||
private int teleportId;
|
||||
private int teleportId, relativeMask;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
@@ -46,7 +46,7 @@ public final class SetPlayerPositionRotationPacket implements FallbackPacket {
|
||||
byteBuf.writeDouble(z);
|
||||
byteBuf.writeFloat(yaw);
|
||||
byteBuf.writeFloat(pitch);
|
||||
byteBuf.writeBoolean(onGround);
|
||||
byteBuf.writeByte(relativeMask);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
|
||||
writeVarInt(byteBuf, teleportId);
|
||||
@@ -63,7 +63,7 @@ public final class SetPlayerPositionRotationPacket implements FallbackPacket {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account 1.7 bounding box
|
||||
y = byteBuf.readDouble(); // Account for 1.7 bounding box
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TeleportEntityPacket implements FallbackPacket {
|
||||
private int entityId;
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
writeVarInt(byteBuf, entityId);
|
||||
} else {
|
||||
byteBuf.writeInt(entityId);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
|
||||
byteBuf.writeDouble(x);
|
||||
byteBuf.writeDouble(y);
|
||||
byteBuf.writeDouble(z);
|
||||
} else {
|
||||
byteBuf.writeInt((int) (x * 32D));
|
||||
byteBuf.writeInt((int) (y * 32D));
|
||||
byteBuf.writeInt((int) (z * 32D));
|
||||
}
|
||||
|
||||
byteBuf.writeByte(0); // pitch or yaw
|
||||
byteBuf.writeByte(0); // yaw or pitch
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeBoolean(onGround);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -54,7 +54,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
|
||||
for (final BlockUpdate block : blockUpdates) {
|
||||
byteBuf.writeShort(block.getLegacyBlockState());
|
||||
final int blockId = block.getType().getId(protocolVersion);
|
||||
final int blockId = block.getBlockType().getId(protocolVersion);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
writeVarInt(byteBuf, blockId);
|
||||
} else {
|
||||
@@ -82,7 +82,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
|
||||
for (final BlockUpdate block : blockUpdates) {
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
|
||||
final int shiftedBlockId = block.getBlockType().getId(protocolVersion) << 12;
|
||||
final long positionIdValue = shiftedBlockId | block.getBlockState();
|
||||
writeVarLong(byteBuf, positionIdValue);
|
||||
}
|
||||
|
||||
+2
@@ -21,11 +21,13 @@ import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class VehicleMovePacket implements FallbackPacket {
|
||||
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackSessionHandler implements FallbackPacketListener {
|
||||
protected final FallbackUser user;
|
||||
protected final String username;
|
||||
|
||||
protected static final Random RANDOM = new Random();
|
||||
|
||||
protected void checkState(final boolean state, final String failReason) {
|
||||
// Fails the verification if the condition is not met
|
||||
if (!state) {
|
||||
user.fail(failReason);
|
||||
}
|
||||
}
|
||||
|
||||
protected final void finishVerification() {
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
|
||||
user.getInetAddress(), user.getOfflineUuid(), user.getLoginTimer().getStart()));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
|
||||
username, user.getOfflineUuid(), user, user.getLoginTimer().delay()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (transferToOrigin != null
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Send the transfer packet to the player (and close the channel if on Java Edition)
|
||||
if (user.isGeyser()) {
|
||||
user.write(transferToOrigin);
|
||||
// Make sure we cannot receive any more packets from the player
|
||||
user.getPipeline().remove(FallbackPacketDecoder.class);
|
||||
user.getPipeline().remove(FallbackPacketEncoder.class);
|
||||
} else {
|
||||
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
|
||||
}
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
|
||||
.replace("<username>", username)
|
||||
.replace("<time-taken>", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void checkClientInformation(final @NotNull ClientInformationPacket clientSettings) {
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientSettings.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
checkState((clientSettings.getSkinParts() & 0x80) == 0, "sent unused bit flag");
|
||||
|
||||
// Mark the ClientSettings packet as received
|
||||
user.setReceivedClientSettings(true);
|
||||
}
|
||||
|
||||
protected final void checkPluginMessage(final @NotNull PluginMessagePacket pluginMessage) {
|
||||
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
|
||||
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
|
||||
|
||||
// Skip this payload if it does not contain client brand information
|
||||
if (!usingModernChannel && !usingLegacyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player isn't sending the client brand multiple times
|
||||
checkState(!user.isReceivedPluginMessage(), "sent duplicate plugin message");
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
checkState(usingLegacyChannel || v1_13, "invalid brand channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getData());
|
||||
}
|
||||
|
||||
// Mark the PluginMessage packet as received
|
||||
user.setReceivedPluginMessage(true);
|
||||
}
|
||||
|
||||
protected final void validateClientBrand(final byte @NotNull [] data) {
|
||||
// Check if the client brand is too short. It has to have at least 2 bytes.
|
||||
checkState(data.length > 1, "client brand is too short");
|
||||
// Check if the decoded client brand string is too long
|
||||
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand contains too much data: " + data.length);
|
||||
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
|
||||
String brand = new String(data, StandardCharsets.UTF_8);
|
||||
// Remove the invalid character at the beginning of the client brand
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
brand = brand.substring(1);
|
||||
}
|
||||
// Check for illegal client brands
|
||||
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern: " + brand);
|
||||
}
|
||||
|
||||
protected final void validateClientLocale(final @NotNull String locale) {
|
||||
// Check the client locale by performing a simple regex check
|
||||
// that disallows non-ascii characters by default.
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern: " + locale);
|
||||
}
|
||||
}
|
||||
-211
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SpawnEntityPacket} and {@link SetPassengersPacket} packets are sent to the client,
|
||||
* therefore, making the player enter a boat.
|
||||
* <br>
|
||||
* The boat will be spawned at y 1337.
|
||||
* <br>
|
||||
* After 3 {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets,
|
||||
* the boat is removed.
|
||||
* <br>
|
||||
* Now we can check if the player sends the correct position after the entity is removed.
|
||||
* Please note that we don't tell the client to exit the vehicle, therefore, possibly confusing some bots.
|
||||
* Removing the boat results in the player exiting the vehicle on vanilla clients.
|
||||
* <br>
|
||||
* See more: {@link FallbackVehicleSessionHandler}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, we listen for incoming {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets.
|
||||
* </li>
|
||||
*/
|
||||
public final class FallbackVehicleSessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackVehicleSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
|
||||
// Send the necessary packets to mount the player on the boat vehicle
|
||||
user.delayedWrite(spawnBoatEntity);
|
||||
user.delayedWrite(setBoatPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private int expectedTransactionId;
|
||||
private int paddlePackets, inputPackets, positionPackets, rotationPackets, vehicleMovePackets;
|
||||
private boolean expectMovement, inMinecart, waitingSpawnMinecartTransaction;
|
||||
private @NotNull FallbackPacket removeEntitiesPacket = removeBoatEntities;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
// Either send the player to the CAPTCHA or finish the verification.
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Send the player to the CAPTCHA handler
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
// We can abuse the entity remove mechanic and check for position packets when the entity dies
|
||||
private void handleMovement(double y) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
y -= 1.62f; // Account for 1.7 bounding box
|
||||
}
|
||||
// Check the Y position of the player
|
||||
checkState(y <= IN_AIR_Y_POSITION, "invalid y position");
|
||||
// Mark this check as successful if the player sent a few position packets
|
||||
if (positionPackets++ > Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets()) {
|
||||
if (user.isGeyser() || inMinecart) {
|
||||
markSuccess();
|
||||
} else {
|
||||
spawnMinecart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRotation() {
|
||||
// Once the player sent enough packets, go to the next stage
|
||||
final int minimumPackets = Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets();
|
||||
if (paddlePackets > minimumPackets
|
||||
&& inputPackets > minimumPackets
|
||||
&& rotationPackets > minimumPackets
|
||||
&& vehicleMovePackets > minimumPackets) {
|
||||
// Remove the entity
|
||||
// The next y coordinate the player will send is going
|
||||
// to be the vehicle spawn position (y 64 in this case).
|
||||
user.write(removeEntitiesPacket);
|
||||
// Listen for next movement packet(s)
|
||||
expectMovement = true;
|
||||
}
|
||||
rotationPackets++;
|
||||
}
|
||||
|
||||
private void spawnMinecart() {
|
||||
expectMovement = false;
|
||||
paddlePackets = inputPackets = positionPackets = rotationPackets = 0;
|
||||
waitingSpawnMinecartTransaction = true;
|
||||
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
|
||||
// Use transaction packets to confirm that the entity has spawned
|
||||
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
|
||||
user.delayedWrite(spawnMinecartEntity);
|
||||
user.delayedWrite(setMinecartPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof TransactionPacket) {
|
||||
// Waiting for transaction to make sure the player is riding on the minecart and not on the boat
|
||||
if (waitingSpawnMinecartTransaction) {
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
// Also check if the transaction ID matches the expected ID
|
||||
checkState(expectedTransactionId == transaction.getTransactionId(),
|
||||
"expected T ID " + expectedTransactionId + ", but got " + transaction.getTransactionId());
|
||||
|
||||
waitingSpawnMinecartTransaction = false;
|
||||
expectedTransactionId = 0;
|
||||
removeEntitiesPacket = removeMinecartEntities;
|
||||
inMinecart = true;
|
||||
}
|
||||
} else if (packet instanceof SetPlayerPositionRotationPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
|
||||
handleMovement(posRot.getY());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerPositionPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
|
||||
handleMovement(position.getY());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerRotationPacket) {
|
||||
if (!expectMovement && !user.isGeyser()) {
|
||||
handleRotation();
|
||||
}
|
||||
} else if (packet instanceof PaddleBoatPacket) {
|
||||
checkState(!inMinecart, "invalid packet order (unexpected PaddleBoatPacket)");
|
||||
paddlePackets++;
|
||||
} else if (packet instanceof VehicleMovePacket) {
|
||||
checkState(!inMinecart, "invalid packet order (unexpected VehicleMovePacket)");
|
||||
vehicleMovePackets++;
|
||||
} else if (packet instanceof PlayerInputPacket && !expectMovement) {
|
||||
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
|
||||
|
||||
// Check if the player is sending invalid vehicle speed values
|
||||
final float forward = Math.abs(playerInput.getForward());
|
||||
final float sideways = Math.abs(playerInput.getSideways());
|
||||
final float maxVehicleSpeed = user.isGeyser() ? 1 : 0.98f;
|
||||
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
|
||||
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
|
||||
|
||||
// Only mark this packet as correct if the player is not moving the vehicle
|
||||
if (playerInput.isJump() || playerInput.isUnmount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bedrock users do not send SetPlayerPositionRotation and SetPlayerRotation packets
|
||||
// Don't ask me why; Microsoft is doing some *fascinating* things with Bedrock...
|
||||
if (user.isGeyser()) {
|
||||
handleRotation();
|
||||
} else {
|
||||
checkState(rotationPackets >= inputPackets,
|
||||
"illegal packet order; r/i " + rotationPackets + "/" + inputPackets);
|
||||
}
|
||||
|
||||
// 1.8 and below do not have PaddleBoat packets, so we simply exempt them from the PaddleBoat check.
|
||||
// Clients don't send PaddleBoat & VehicleMovePacket packets while riding minecarts.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0 || inMinecart) {
|
||||
paddlePackets++;
|
||||
vehicleMovePackets++;
|
||||
} else {
|
||||
checkState(paddlePackets >= inputPackets,
|
||||
"illegal packet order; i/p " + inputPackets + "/" + paddlePackets);
|
||||
checkState(vehicleMovePackets == inputPackets,
|
||||
"illegal packet order; i/v " + inputPackets + "/" + vehicleMovePackets);
|
||||
}
|
||||
inputPackets++;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-24
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -24,29 +24,18 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.MapCaptchaInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetContainerSlotPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPlayerPositionPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPlayerPositionRotationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SystemChatPacket;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SetContainerSlotPacket} and {@link MapDataPacket} packets are sent to the client,
|
||||
* therefore, setting the player's item to a map with a code on it (CAPTCHA).
|
||||
* <br>
|
||||
* See more: {@link FallbackCAPTCHASessionHandler}, {@link MapCaptchaInfo}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, we wait for the player to enter the {@link FallbackCAPTCHASessionHandler#answer} in chat.
|
||||
* </li>
|
||||
*/
|
||||
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
|
||||
public final class FallbackCaptchaHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackCAPTCHASessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username) {
|
||||
super(user, username);
|
||||
public FallbackCaptchaHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Disconnect the player if there is no CAPTCHA available at the moment
|
||||
if (!CaptchaPreparer.isCaptchaAvailable()) {
|
||||
@@ -58,8 +47,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
|
||||
// If the player is on Java, set the 5th slot (ID 4) in the player's hotbar to the map
|
||||
// If the player is on Bedrock, set the 1st slot (ID 0) in the player's hotbar to the map
|
||||
user.delayedWrite(new SetContainerSlotPacket(user.isGeyser() ? 36 : 40, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetContainerSlotPacket.MAP_NBT));
|
||||
final int slotId = user.isGeyser() ? 36 : 40;
|
||||
user.delayedWrite(new SetContainerSlotPacket(0, slotId, 1, ItemType.FILLED_MAP, MAP_ITEM_NBT));
|
||||
// Send random captcha to the player
|
||||
final MapCaptchaInfo captcha = CaptchaPreparer.getRandomCaptcha();
|
||||
this.answer = captcha.getAnswer().toLowerCase();
|
||||
@@ -67,7 +56,7 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
|
||||
user.delayedWrite(user.isGeyser() ? NO_MOVE_ABILITIES_BEDROCK : NO_MOVE_ABILITIES);
|
||||
// Make sure the player knows that they have to enter the code in chat
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
@@ -100,9 +89,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
if (Sonar.get().getConfig().getVerification().getGamemode().isSurvivalOrAdventure()) {
|
||||
final long difference = maxDuration - user.getLoginTimer().delay();
|
||||
final int index = (int) (difference / 1000D);
|
||||
// Make sure we can actually safely get and send the packet
|
||||
// Make sure we can safely get and send the packet
|
||||
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
|
||||
// Send the countdown using the experience bar
|
||||
user.write(xpCountdown[index]);
|
||||
}
|
||||
lastCountdownIndex = index;
|
||||
+85
-76
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -28,17 +28,19 @@ import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Game
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackGravitySessionHandler extends FallbackSessionHandler {
|
||||
public final class FallbackGravityHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackGravitySessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username) {
|
||||
super(user, username);
|
||||
public FallbackGravityHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull FallbackPreJoinHandler preJoinHandler) {
|
||||
super(user);
|
||||
|
||||
this.preJoinHandler = preJoinHandler;
|
||||
// Bedrock users start falling immediately
|
||||
this.canFall = user.isGeyser();
|
||||
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
|
||||
this.enableGravityCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.enableCollisionsCheck = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
this.enableCollisionsCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
|
||||
// FIXME: 1.18.2-1.19.3 weird loading world issue
|
||||
// First, write the JoinGame packet to the buffer
|
||||
user.delayedWrite(joinGame);
|
||||
// Then, write the ClientAbilities packet to the buffer
|
||||
@@ -46,12 +48,18 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
if (Sonar.get().getConfig().getVerification().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
}
|
||||
// Write the PositionLook packet to the buffer
|
||||
user.delayedWrite(spawnPosition);
|
||||
// Write the DefaultSpawnPosition packet to the buffer
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) >= 0) {
|
||||
user.delayedWrite(defaultSpawnPosition);
|
||||
}
|
||||
// Teleport the player to the position where we're starting to check them
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
user.delayedWrite(spawnPosition);
|
||||
user.delayedWrite(fallStartPosition);
|
||||
} else {
|
||||
// 1.7.2-1.7.10 clients do not have relative teleports
|
||||
user.delayedWrite(fallStartPositionLegacy);
|
||||
}
|
||||
// 1.20.3+ introduced game events
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
@@ -60,42 +68,37 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
// Teleport player into an empty world by sending an empty chunk packet
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
// Spawn the invisible platform below the player
|
||||
if (this.enableCollisionsCheck) {
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
if (enableCollisionsCheck) {
|
||||
final int index = RANDOM.nextInt(BLOCKS_PACKETS.length);
|
||||
blockHeight = POSSIBLE_BLOCK_TYPES[index].getBlockHeight();
|
||||
user.delayedWrite(BLOCKS_PACKETS[index]);
|
||||
}
|
||||
// Send all packets at once
|
||||
user.getChannel().flush();
|
||||
|
||||
// 1.8 and below don't have TeleportConfirm packets, which is why we're skipping that check.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
|
||||
markTeleported();
|
||||
// Enable the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
}
|
||||
|
||||
private final FallbackPreJoinHandler preJoinHandler;
|
||||
private final boolean enableGravityCheck, enableCollisionsCheck;
|
||||
private boolean teleported, checkMovement;
|
||||
private double y, deltaY;
|
||||
private int movementTick;
|
||||
private boolean teleported, canFall, checkMovement;
|
||||
private double y, deltaY, blockHeight;
|
||||
private int movementTick, expectedTeleportId = FIRST_TELEPORT_ID;
|
||||
|
||||
private void markTeleported() {
|
||||
// Activate the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
|
||||
private void markSuccess(final boolean forceCAPTCHA) {
|
||||
private void markSuccess() {
|
||||
// Force-stop the movement checks
|
||||
teleported = false;
|
||||
|
||||
// Check if the player sent all necessary packets
|
||||
checkState(user.isReceivedClientSettings(), "didn't send client settings");
|
||||
// Don't check Geyser players for plugin messages
|
||||
if (!user.isGeyser()) {
|
||||
checkState(user.isReceivedPluginMessage(), "didn't send plugin message");
|
||||
// Exempt pre-1.20.2 since they've already passed that check in the configuration phase
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
preJoinHandler.checkClientInformation();
|
||||
}
|
||||
|
||||
// Send the player to the protocol check
|
||||
// Send the player to the next verification handler
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackProtocolSessionHandler(user, username, forceCAPTCHA));
|
||||
decoder.setListener(new FallbackProtocolHandler(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,38 +122,34 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
checkState(!teleported, "duplicate teleport confirm");
|
||||
// Check if the teleport ID matches the expected ID
|
||||
final int teleportId = teleportConfirm.getTeleportId();
|
||||
checkState(teleportId == TELEPORT_ID,
|
||||
"expected TP ID " + TELEPORT_ID + ", but got " + teleportId);
|
||||
checkState(teleportId == expectedTeleportId,
|
||||
"expected TP ID " + expectedTeleportId + ", but got " + teleportId);
|
||||
|
||||
markTeleported();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the play phase.
|
||||
else if (packet instanceof ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
// The first teleport ID is not useful for us in this context, skip it
|
||||
if (expectedTeleportId == FIRST_TELEPORT_ID) {
|
||||
expectedTeleportId = SECOND_TELEPORT_ID;
|
||||
} else {
|
||||
// Enable the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
} else if (packet instanceof ClientInformationPacket
|
||||
|| packet instanceof PluginMessagePacket) {
|
||||
// Pass these packets back to the login handler
|
||||
preJoinHandler.handle(packet);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMovement(final double x, final double y, final double z, final boolean isOnGround) {
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!checkMovement) {
|
||||
// No need to continue checking if the gravity and collision checks are disabled
|
||||
// Continue with the next stage of the verification
|
||||
if (!enableGravityCheck && !enableCollisionsCheck) {
|
||||
markSuccess(false);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
if (x == SPAWN_X_POSITION && z == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
checkMovement = true;
|
||||
movementTick = 0;
|
||||
// Synchronize the Y coordinate
|
||||
this.y = dynamicSpawnYPosition;
|
||||
}
|
||||
// Synchronize the Y coordinate
|
||||
this.y = dynamicSpawnYPosition;
|
||||
checkMovement = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,81 +161,91 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
Sonar.get().getLogger().info("{}: {}/{}/{}, ly={}/dy={}, {}",
|
||||
username, x, y, z, lastY, deltaY, isOnGround);
|
||||
Sonar.get().getLogger().info("{}: {}/{}/{} ly={}, dy={}, h={}, g={}",
|
||||
user.getUsername(), x, y, z, lastY, deltaY, blockHeight, isOnGround);
|
||||
}
|
||||
|
||||
// Ensure that the player's Y coordinate is above the collision platform
|
||||
checkState(y >= DEFAULT_Y_COLLIDE_POSITION,
|
||||
"fell through blocks: " + y + "/" + deltaY + "/" + movementTick);
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkState(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkState(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
checkState(Math.abs(Math.abs(x) - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "illegal x offset: " + x);
|
||||
checkState(Math.abs(Math.abs(z) - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "illegal z offset: " + z);
|
||||
|
||||
if (!isOnGround) {
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
// We have to account for this or the player will falsely fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Reset the movement tick as a safety measure
|
||||
movementTick = 0;
|
||||
checkState(movementTick == 0, "illegal y motion: " + movementTick);
|
||||
// 1.7 clients immediately start falling after this packet
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
movementTick++;
|
||||
}
|
||||
// We've received the first position packet; the player will now start falling
|
||||
canFall = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're actually expecting the player to fall at this point in time
|
||||
checkState(canFall, "unexpected y motion: " + deltaY);
|
||||
|
||||
// The movement tick should not be ignored when gravity check is disabled.
|
||||
movementTick++;
|
||||
|
||||
if (enableGravityCheck) {
|
||||
// Ensure that the player is above the collision platform
|
||||
checkState(y > DEFAULT_Y_COLLIDE_POSITION,
|
||||
"fell through blocks; y: " + y + " deltaY: " + deltaY + " tick: " + movementTick);
|
||||
|
||||
// Predict the player's current motion based on the last motion
|
||||
// https://minecraft.wiki/w/Entity#Motion_of_entities
|
||||
final double predicted = (lastDeltaY - 0.08) * 0.98f;
|
||||
final double difference = Math.abs(deltaY - predicted);
|
||||
|
||||
// Check if the difference between the predicted motion and
|
||||
// the actual motion is greater than our minimum threshold.
|
||||
// Check if the difference between the predicted and actual motion is too large
|
||||
if (difference > 1e-7) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
user.setForceCaptcha(true);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
user.fail("incorrect gravity; predicted: " + predicted + " deltaY: " + deltaY + " y: " + y);
|
||||
fail("incorrect gravity: " + predicted + "/ " + deltaY + "/" + y);
|
||||
}
|
||||
|
||||
// The player is obeying gravity, go on to the next stage if the collision check is disabled.
|
||||
if (movementTick == maxMovementTick && !enableCollisionsCheck) {
|
||||
markSuccess(false);
|
||||
if (!enableCollisionsCheck && movementTick == maxMovementTick) {
|
||||
markSuccess();
|
||||
}
|
||||
}
|
||||
} else if (enableCollisionsCheck) {
|
||||
// Make sure the player has actually moved before reaching the platform
|
||||
if (movementTick < maxMovementTick) {
|
||||
if (enableGravityCheck && movementTick < maxMovementTick) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
user.setForceCaptcha(true);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
user.fail("illegal collision tick; tick: " + movementTick);
|
||||
fail("illegal collision tick: " + movementTick + "/" + blockHeight);
|
||||
}
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockHeight) - y;
|
||||
// 1.7 sends the head position instead of the AABB minY
|
||||
// This little hack accounts for the offset of approximately 1.62
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f;
|
||||
}
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
if (collisionOffsetY < -0.03) {
|
||||
if (collisionOffsetY < -3e-2) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
user.setForceCaptcha(true);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
user.fail("illegal collision; offset: " + collisionOffsetY + " tick: " + movementTick + " y: " + y);
|
||||
fail("illegal collision: " + collisionOffsetY + "/" + y + "/" + movementTick + "/" + blockHeight);
|
||||
}
|
||||
// The player has collided with the blocks, go on to the next stage
|
||||
markSuccess(false);
|
||||
markSuccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
-52
@@ -15,9 +15,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
@@ -29,39 +30,19 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformatio
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.KeepAlivePacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* A {@link KeepAlivePacket} packet is sent to the client*
|
||||
* <br>
|
||||
* See more: {@link FallbackLoginSessionHandler#initialize18()}
|
||||
* </li>
|
||||
* <li>
|
||||
* After the KeepAlive packet is validated, the client enters the configuration phase.**
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, the session handler is set to the {@link FallbackGravitySessionHandler}.
|
||||
* <br>
|
||||
* See more: {@link FallbackLoginSessionHandler#markSuccess()}
|
||||
* </li>
|
||||
* <br>
|
||||
* * The KeepAlive check is skipped on 1.7, as KeepAlive packets don't exist during the LOGIN state.
|
||||
* <br>
|
||||
* ** The internals of the configuration phase were not mentioned.
|
||||
* Find out more about the client configuration and registry synchronization:
|
||||
* {@link #synchronizeClientRegistry()}, {@link #updateEncoderDecoderState(FallbackPacketRegistry)}
|
||||
*/
|
||||
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
public class FallbackPreJoinHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackLoginSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username) {
|
||||
super(user, username);
|
||||
public FallbackPreJoinHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Start initializing the actual join process for pre-1.20.2 clients
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
@@ -70,11 +51,12 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
user.getChannel().eventLoop().schedule(this::markSuccess, 100L, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
initialize18();
|
||||
sendKeepAlive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean receivedClientInfo, receivedClientBrand;
|
||||
private boolean acknowledgedLogin;
|
||||
private int expectedKeepAliveId;
|
||||
|
||||
@@ -83,31 +65,35 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
* is active and legitimate, thereby preventing bot connections that
|
||||
* could flood the server with login attempts and other unwanted traffic.
|
||||
*/
|
||||
private void initialize18() {
|
||||
private void sendKeepAlive() {
|
||||
// Send a KeepAlive packet with a random ID
|
||||
expectedKeepAliveId = RANDOM.nextInt();
|
||||
user.write(new KeepAlivePacket(expectedKeepAliveId));
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Make sure we can actually switch over to the next check
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
// Pass the player to the next verification handler
|
||||
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username);
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(gravitySessionHandler);
|
||||
}
|
||||
|
||||
private void markAcknowledged() {
|
||||
acknowledgedLogin = true;
|
||||
|
||||
// Update state, so we're able to send/receive configuration packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
synchronizeClientRegistry();
|
||||
// Write the FinishConfiguration packet to the buffer
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Set decoder state to actually catch all packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
}
|
||||
|
||||
protected final void checkClientInformation() {
|
||||
checkState(receivedClientInfo, "didn't send client settings");
|
||||
// Don't check Geyser players for plugin messages as they don't have them
|
||||
if (!user.isGeyser()) {
|
||||
checkState(receivedClientBrand, "didn't send plugin message");
|
||||
}
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next verification handler
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackGravityHandler(user, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,8 +111,7 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
// while the player is in the "Downloading terrain" screen.
|
||||
expectedKeepAliveId = 0;
|
||||
|
||||
// Spawn the player in the virtual world if the client does not need
|
||||
// any configuration (pre-1.20.2).
|
||||
// Immediately verify the player if they do not need any configuration (pre-1.20.2)
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
markSuccess();
|
||||
}
|
||||
@@ -135,21 +120,52 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
checkState(!acknowledgedLogin, "sent duplicate login ack");
|
||||
markAcknowledged();
|
||||
} else if (packet instanceof FinishConfigurationPacket) {
|
||||
checkClientInformation();
|
||||
// Update the encoder and decoder state because we're currently in the CONFIG state
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
markSuccess();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the configuration phase.
|
||||
else if (packet instanceof ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof ClientInformationPacket) {
|
||||
final ClientInformationPacket clientInformation = (ClientInformationPacket) packet;
|
||||
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientInformation.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
// TODO: check if this causes issues with cosmetics in pvp clients
|
||||
checkState((clientInformation.getSkinParts() & 0x80) == 0,
|
||||
"sent unused bit flag: " + clientInformation.getSkinParts());
|
||||
|
||||
receivedClientInfo = true;
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
|
||||
|
||||
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
|
||||
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
|
||||
|
||||
// Skip this payload if it does not contain client brand information
|
||||
if (!usingModernChannel && !usingLegacyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player isn't sending the client brand multiple times
|
||||
checkState(!receivedClientBrand, "sent duplicate plugin message");
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
checkState(usingLegacyChannel || user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0,
|
||||
"illegal PluginMessage channel: " + pluginMessage.getChannel());
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getData());
|
||||
}
|
||||
|
||||
receivedClientBrand = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
final var encoder = (FallbackPacketEncoder) user.getPipeline().get(FallbackPacketEncoder.class);
|
||||
final var encoder = user.getPipeline().get(FallbackPacketEncoder.class);
|
||||
// Update the packet registry state in the encoder and decoder pipelines
|
||||
decoder.updateRegistry(registry);
|
||||
encoder.updateRegistry(registry);
|
||||
@@ -168,4 +184,30 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
user.delayedWrite(REGISTRY_SYNC_LEGACY);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateClientBrand(final byte @NotNull [] data) {
|
||||
// Check if the client brand is too short. It has to have at least 2 bytes.
|
||||
checkState(data.length > 1, "client brand is too short");
|
||||
// Check if the decoded client brand string is too long
|
||||
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand contains too much data: " + data.length);
|
||||
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
|
||||
String brand = new String(data, StandardCharsets.UTF_8);
|
||||
// Remove the invalid character at the beginning of the client brand
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
brand = brand.substring(1);
|
||||
}
|
||||
// Check for illegal client brands
|
||||
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern: " + brand);
|
||||
}
|
||||
|
||||
private void validateClientLocale(final @NotNull String locale) {
|
||||
// Check the client locale by performing a simple regex check
|
||||
// that disallows non-ascii characters by default.
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern: " + locale);
|
||||
}
|
||||
}
|
||||
+13
-12
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -28,22 +28,18 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetHeldItemPacke
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.TransactionPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.CACHED_HELD_ITEM_RESET;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.PLAYER_ENTITY_ID;
|
||||
|
||||
public final class FallbackProtocolSessionHandler extends FallbackSessionHandler {
|
||||
public final class FallbackProtocolHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackProtocolSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
public FallbackProtocolHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Immediately send the player the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private boolean waitingSwingArm, waitingSlotConfirm;
|
||||
private short expectedTransactionId;
|
||||
private int currentClientSlotId, expectedSlotId = -1;
|
||||
@@ -69,6 +65,9 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
// Move the player's slot by 4 (slot limit divided by 2),
|
||||
// and then modulo it by the slot limit (8) to ensure that we don't send invalid slot IDs.
|
||||
expectedSlotId = (currentClientSlotId + 4) % 8;
|
||||
// Reset the player's slot to 4, so we don't get false positives if the client changes the slot
|
||||
// This doesn't *really* work for 1.7-1.8 clients, but it's better to still send a packet if they're de-sync
|
||||
user.delayedWrite(CACHED_HELD_ITEM_RESET);
|
||||
// Send two SetHeldItem packets with the same slot to check if the player responds with the correct slot.
|
||||
// By vanilla protocol, the client does not respond to duplicate SetHeldItem packets.
|
||||
// We can take advantage of this by sending two packets with the same content to check for a valid response.
|
||||
@@ -95,9 +94,9 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Pass the player to the next best verification handler
|
||||
if (Sonar.get().getConfig().getVerification().getVehicle().isEnabled()) {
|
||||
decoder.setListener(new FallbackVehicleSessionHandler(user, username, forceCAPTCHA));
|
||||
} else if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
|
||||
decoder.setListener(new FallbackVehicleHandler(user));
|
||||
} else if (user.isForceCaptcha() || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCaptchaHandler(user));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
@@ -111,6 +110,8 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the window ID is valid
|
||||
checkState(transaction.getWindowId() == 0, "wrong window ID " + transaction.getWindowId());
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVehicleHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackVehicleHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Send the necessary packets to mount the player on the boat vehicle
|
||||
user.delayedWrite(spawnBoatEntity);
|
||||
user.delayedWrite(setBoatPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private int expectedTransactionId;
|
||||
private int paddlePackets, inputPackets, positionPackets, rotationPackets, vehicleMovePackets;
|
||||
private boolean expectMovement, inVoid, inMinecart, waitingSpawnMinecart, waitingRemoveMinecart;
|
||||
private double minecartMotion, minecartY = IN_AIR_Y_POSITION;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (user.isForceCaptcha() || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
// Either send the player to the CAPTCHA or finish the verification.
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Send the player to the CAPTCHA handler
|
||||
decoder.setListener(new FallbackCaptchaHandler(user));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
// We can abuse the entity remove mechanic and check for position packets when the entity dies
|
||||
private void handleMovement(double y, final boolean onGround) {
|
||||
// The player cannot be on the ground if the Y position is less than 0
|
||||
checkState(!onGround || !inVoid || waitingRemoveMinecart, "invalid ground state: " + y);
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
y -= 1.62f; // Account for 1.7 bounding box
|
||||
}
|
||||
// Check the Y position of the player
|
||||
final double minimumY = inVoid ? IN_VOID_Y_POSITION : IN_AIR_Y_POSITION;
|
||||
checkState(y <= minimumY, "illegal y position: " + y + "/" + minimumY);
|
||||
// Mark this check as successful if the player sent a few position packets
|
||||
if (positionPackets++ > Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets()) {
|
||||
if (user.isGeyser() || inMinecart || inVoid) {
|
||||
markSuccess();
|
||||
} else {
|
||||
spawnMinecart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRotation() {
|
||||
// Once the player sent enough packets, go to the next stage
|
||||
final int minimumPackets = Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets();
|
||||
if (paddlePackets > minimumPackets
|
||||
&& inputPackets > minimumPackets
|
||||
&& rotationPackets > minimumPackets
|
||||
&& vehicleMovePackets > minimumPackets) {
|
||||
// If the player is riding a Minecart, teleport the entity to < -100
|
||||
// to see how the player reacts to the invalid position of the entity.
|
||||
if (inMinecart) {
|
||||
user.delayedWrite(teleportMinecart);
|
||||
// We're using transactions to avoid false positives with lag
|
||||
waitingRemoveMinecart = true;
|
||||
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
|
||||
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
|
||||
user.getChannel().flush();
|
||||
} else {
|
||||
// If the player is riding a boat, remove the entity
|
||||
// The next Y coordinate the player will send is going
|
||||
// to be the vehicle spawn position (64 in this case).
|
||||
user.write(removeBoat);
|
||||
}
|
||||
// Listen for next movement packet(s)
|
||||
expectMovement = true;
|
||||
}
|
||||
rotationPackets++;
|
||||
}
|
||||
|
||||
private void spawnMinecart() {
|
||||
expectMovement = false;
|
||||
paddlePackets = inputPackets = positionPackets = rotationPackets = 0;
|
||||
// Use transaction packets to confirm that the entity has spawned
|
||||
waitingSpawnMinecart = true;
|
||||
minecartMotion = 0.03999999910593033D * 2 * -1;
|
||||
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
|
||||
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
|
||||
user.delayedWrite(spawnMinecartEntity);
|
||||
user.delayedWrite(setMinecartPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof TransactionPacket) {
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the window ID is valid
|
||||
checkState(transaction.getWindowId() == 0, "wrong window ID " + transaction.getWindowId());
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
// Also check if the transaction ID matches the expected ID
|
||||
checkState(expectedTransactionId == transaction.getTransactionId(),
|
||||
"expected T ID " + expectedTransactionId + ", but got " + transaction.getTransactionId());
|
||||
|
||||
// We're "lag compensating" the Minecart to avoid false positives
|
||||
if (waitingSpawnMinecart) {
|
||||
waitingSpawnMinecart = false;
|
||||
inMinecart = true;
|
||||
} else if (waitingRemoveMinecart) {
|
||||
waitingRemoveMinecart = inMinecart = false;
|
||||
inVoid = true;
|
||||
positionPackets = 0;
|
||||
}
|
||||
|
||||
expectedTransactionId = 0;
|
||||
} else if (packet instanceof SetPlayerPositionRotationPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
|
||||
handleMovement(posRot.getY(), posRot.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerPositionPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
|
||||
handleMovement(position.getY(), position.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerRotationPacket) {
|
||||
if (!expectMovement && !user.isGeyser()) {
|
||||
handleRotation();
|
||||
}
|
||||
} else if (packet instanceof PaddleBoatPacket) {
|
||||
checkState(!inMinecart, "invalid packet order (unexpected PaddleBoatPacket)");
|
||||
paddlePackets++;
|
||||
} else if (packet instanceof VehicleMovePacket) {
|
||||
checkState(!inMinecart, "invalid packet order (unexpected VehicleMovePacket)");
|
||||
final VehicleMovePacket vehicleMove = (VehicleMovePacket) packet;
|
||||
// Check the Y position of the vehicle
|
||||
checkState(vehicleMove.getY() <= IN_AIR_Y_POSITION, "invalid y position: " + vehicleMove.getY());
|
||||
if (!user.isGeyser()) {
|
||||
// Predict the Y motion of the minecart
|
||||
final double lastMinecartMotion = minecartMotion;
|
||||
final double lastMinecartY = minecartY;
|
||||
minecartY = vehicleMove.getY();
|
||||
minecartMotion = minecartY - lastMinecartY;
|
||||
final double predicted = lastMinecartMotion - 0.03999999910593033D;
|
||||
final double difference = Math.abs(minecartMotion - predicted);
|
||||
// Check if the difference between the predicted and actual motion is too large
|
||||
checkState(difference < 1e-7,
|
||||
"invalid vehicle gravity: " + predicted + "/" + minecartMotion + "/" + difference);
|
||||
}
|
||||
vehicleMovePackets++;
|
||||
} else if (packet instanceof PlayerInputPacket) {
|
||||
if (!expectMovement) {
|
||||
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
|
||||
|
||||
// Check if the player is sending invalid vehicle speed values
|
||||
final float forward = Math.abs(playerInput.getForward());
|
||||
final float sideways = Math.abs(playerInput.getSideways());
|
||||
final float maxVehicleSpeed = user.isGeyser() ? 1 : 0.98f;
|
||||
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
|
||||
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
|
||||
|
||||
// Only mark this packet as correct if the player is not moving the vehicle
|
||||
if (playerInput.isJump() || playerInput.isUnmount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bedrock users do not send SetPlayerPositionRotation and SetPlayerRotation packets
|
||||
// Don't ask me why; Microsoft is doing some *fascinating* things with Bedrock...
|
||||
if (user.isGeyser()) {
|
||||
handleRotation();
|
||||
} else {
|
||||
checkState(rotationPackets >= inputPackets,
|
||||
"illegal packet order; r/i " + rotationPackets + "/" + inputPackets);
|
||||
}
|
||||
|
||||
// 1.8 and below do not have PaddleBoat packets, so we simply exempt them from the PaddleBoat check.
|
||||
// Clients don't send PaddleBoat & VehicleMovePacket packets while riding minecarts.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0 || inMinecart) {
|
||||
paddlePackets++;
|
||||
vehicleMovePackets++;
|
||||
} else {
|
||||
checkState(paddlePackets >= inputPackets,
|
||||
"illegal packet order; i/p " + inputPackets + "/" + paddlePackets);
|
||||
checkState(vehicleMovePackets >= inputPackets,
|
||||
"illegal packet order; i/v " + inputPackets + "/" + vehicleMovePackets);
|
||||
}
|
||||
inputPackets++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.verification;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
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.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_5;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
protected final FallbackUser user;
|
||||
|
||||
protected static final Random RANDOM = new Random();
|
||||
|
||||
protected final void finishVerification() {
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
|
||||
user.getFingerprint(), user.getLoginTimer().getStart()));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(user, user.getLoginTimer()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (transferToOrigin != null
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Send the transfer packet to the player (and close the channel if on Java Edition)
|
||||
if (user.isGeyser()) {
|
||||
user.write(transferToOrigin);
|
||||
// Make sure we cannot receive any more packets from the player
|
||||
user.getPipeline().remove(FallbackPacketDecoder.class);
|
||||
user.getPipeline().remove(FallbackPacketEncoder.class);
|
||||
user.getPipeline().remove(FallbackVarInt21FrameDecoder.class);
|
||||
user.getPipeline().remove(FallbackVarIntLengthEncoder.class);
|
||||
} else {
|
||||
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
|
||||
}
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
|
||||
.replace("<username>", user.getUsername())
|
||||
.replace("<time-taken>", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void fail(final @NotNull String reason) {
|
||||
GlobalSonarStatistics.totalFailedVerifications++;
|
||||
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
|
||||
.replace("<username>", user.getUsername())
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(user.getProtocolVersion().getProtocol()))
|
||||
.replace("<reason>", reason));
|
||||
}
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(user, reason));
|
||||
|
||||
// Use a label, so we can easily add more code beneath this method in the future
|
||||
blacklist: {
|
||||
final String hostAddress = user.getInetAddress().getHostAddress();
|
||||
final int score = Sonar.get().getFallback().getBlacklist().get(hostAddress, __ -> 0);
|
||||
final int newScore = score + 1;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(hostAddress, newScore);
|
||||
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The player hasn't been blacklisted yet, so skip this iteration
|
||||
if (newScore < limit) break blacklist;
|
||||
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(user));
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
|
||||
.replace("<username>", user.getUsername())
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an exception to avoid further code execution
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
protected final void checkState(final boolean state, final String failReason) {
|
||||
// Fails the verification if the condition is not met
|
||||
if (!state) {
|
||||
fail(failReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -21,6 +21,7 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler;
|
||||
|
||||
// TODO: maybe recode this?
|
||||
/**
|
||||
* This counts all incoming and outgoing traffic.
|
||||
*/
|
||||
|
||||
@@ -44,7 +44,6 @@ public final class BlacklistCommand extends Subcommand {
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
|
||||
@@ -53,15 +52,6 @@ public final class BlacklistCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a warning if the IP is verified but being added to the blacklist
|
||||
if (Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add-warning"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
Sonar.get().getFallback().getBlacklist().put(rawAddress, 1337 /* arbitrarily high number */);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
|
||||
@@ -80,7 +70,6 @@ public final class BlacklistCommand extends Subcommand {
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
|
||||
@@ -88,7 +77,6 @@ public final class BlacklistCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
Sonar.get().getFallback().getBlacklist().invalidate(rawAddress);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
|
||||
@@ -107,7 +95,6 @@ public final class BlacklistCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
Sonar.get().getFallback().getBlacklist().invalidateAll();
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.clear"),
|
||||
|
||||
+22
-61
@@ -25,110 +25,72 @@ import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verified",
|
||||
arguments = {"add", "remove", "clear", "size", "history"}
|
||||
arguments = {"add", "remove", "clear", "size"}
|
||||
)
|
||||
public final class VerifiedCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "verified history <IP address>");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove duplicate code
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
// Make sure the IP is verified already
|
||||
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
|
||||
for (final UUID uuid : Sonar.get().getVerifiedPlayerController().getUUIDs(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history-entry"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("uuid", uuid.toString())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "verified remove <IP address>");
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
incorrectUsage(invocation.getSource(), "verified remove <IP address> <username>");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove duplicate code
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
final String hostAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
if (hostAddress == null) return;
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
|
||||
final String username = invocation.getRawArguments()[3];
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
|
||||
if (!Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getVerifiedPlayerController().remove(rawAddress);
|
||||
Sonar.get().getVerifiedPlayerController().remove(fingerprint);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.remove"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
Placeholder.unparsed("ip", hostAddress),
|
||||
Placeholder.unparsed("username", username)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
incorrectUsage(invocation.getSource(), "verified add <IP address> <UUID/username>");
|
||||
incorrectUsage(invocation.getSource(), "verified add <IP address> <username>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
final String hostAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
if (hostAddress == null) return;
|
||||
|
||||
// Try to parse the UUID (from the username, if needed)
|
||||
final String rawUUID = invocation.getRawArguments()[3];
|
||||
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
|
||||
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
|
||||
final String username = invocation.getRawArguments()[3];
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (Sonar.get().getVerifiedPlayerController().has(rawAddress, uuid)) {
|
||||
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.already"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add verified player to the database
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(rawAddress, uuid, timestamp));
|
||||
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(fingerprint, System.currentTimeMillis()));
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
Placeholder.unparsed("ip", hostAddress),
|
||||
Placeholder.unparsed("username", username)));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -142,7 +104,6 @@ public final class VerifiedCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
Sonar.get().getVerifiedPlayerController().clearAll();
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.clear"),
|
||||
|
||||
@@ -106,21 +106,21 @@ public final class ComponentHolder {
|
||||
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();
|
||||
bytes[i] = jsonArray.get(i).getAsNumber().byteValue();
|
||||
}
|
||||
|
||||
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();
|
||||
ints[i] = jsonArray.get(i).getAsNumber().intValue();
|
||||
}
|
||||
|
||||
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();
|
||||
longs[i] = jsonArray.get(i).getAsNumber().longValue();
|
||||
}
|
||||
|
||||
return LongArrayBinaryTag.longArrayBinaryTag(longs);
|
||||
|
||||
@@ -41,21 +41,25 @@ package xyz.jonesdev.sonar.common.util;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import io.netty.util.Version;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_7_2;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
|
||||
@UtilityClass
|
||||
@@ -76,25 +80,6 @@ public class ProtocolUtil {
|
||||
return VAR_INT_LENGTHS[Integer.numberOfLeadingZeros(value)];
|
||||
}
|
||||
|
||||
public static void checkNettyVersion() {
|
||||
final Version version = Version.identify().getOrDefault("netty-all", Version.identify().get("netty-common"));
|
||||
|
||||
// We're pretty much only doing this to avoid incompatibilities on Bukkit,
|
||||
// so we don't really care if the version couldn't be resolved.
|
||||
if (version == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String[] artifactVersion = version.artifactVersion().split("\\.");
|
||||
final int major = Integer.parseInt(artifactVersion[0]);
|
||||
final int minor = Integer.parseInt(artifactVersion[1]);
|
||||
|
||||
// Enforce Netty >4.1.x
|
||||
if (major < 4 || (major == 4 && minor < 1)) {
|
||||
throw new IllegalStateException("Your Netty version is too old to run Sonar! Please use Netty >4.1.x.");
|
||||
}
|
||||
}
|
||||
|
||||
public static int readVarInt(final @NotNull ByteBuf byteBuf) {
|
||||
final int readable = byteBuf.readableBytes();
|
||||
if (readable == 0) {
|
||||
@@ -218,6 +203,41 @@ public class ProtocolUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkNettyVersion() {
|
||||
final Version version = Version.identify().getOrDefault("netty-all", Version.identify().get("netty-common"));
|
||||
|
||||
// We're pretty much only doing this to avoid incompatibilities on Bukkit,
|
||||
// so we don't really care if the version couldn't be resolved.
|
||||
if (version == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String[] artifactVersion = version.artifactVersion().split("\\.");
|
||||
final int major = Integer.parseInt(artifactVersion[0]);
|
||||
final int minor = Integer.parseInt(artifactVersion[1]);
|
||||
|
||||
// Enforce Netty >4.1.x
|
||||
if (major < 4 || (major == 4 && minor < 1)) {
|
||||
throw new IllegalStateException("Your Netty version is too old to run Sonar! Please use Netty >4.1.x.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeWith(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull Object msg) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
|
||||
channel.eventLoop().execute(() -> {
|
||||
channel.config().setAutoRead(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
|
||||
return new UUID(byteBuf.readLong(), byteBuf.readLong());
|
||||
}
|
||||
@@ -227,28 +247,23 @@ public class ProtocolUtil {
|
||||
}
|
||||
|
||||
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) {
|
||||
int length = readVarInt(byteBuf);
|
||||
final int length = readVarInt(byteBuf);
|
||||
checkState(length >= 0, "Got a negative-length array");
|
||||
checkState(length <= cap, "Bad array size");
|
||||
checkState(byteBuf.isReadable(length), "Trying to read an array that is too long");
|
||||
byte[] array = new byte[length];
|
||||
final byte[] array = new byte[length];
|
||||
byteBuf.readBytes(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final ByteBuf byteBuf) throws DecoderException {
|
||||
return readString(byteBuf, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final ByteBuf byteBuf,
|
||||
final int cap) throws DecoderException {
|
||||
public static @NotNull String readString(final ByteBuf byteBuf, final int cap) throws DecoderException {
|
||||
final int length = readVarInt(byteBuf);
|
||||
return readString(byteBuf, cap, length);
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final @NotNull ByteBuf byteBuf,
|
||||
final int cap,
|
||||
final int length) throws DecoderException {
|
||||
private static @NotNull String readString(final @NotNull ByteBuf byteBuf,
|
||||
final int cap,
|
||||
final int length) throws DecoderException {
|
||||
checkState(length >= 0, "Got a negative-length string");
|
||||
checkState(length <= cap * 3, "Bad string size");
|
||||
checkState(byteBuf.isReadable(length), "Tried to read a too-long string");
|
||||
@@ -276,7 +291,7 @@ public class ProtocolUtil {
|
||||
byteBuf.writeInt((int) uuid.getLeastSignificantBits());
|
||||
}
|
||||
|
||||
public static void writeArray(final ByteBuf byteBuf, final byte @NotNull [] bytes) {
|
||||
public static void writeByteArray(final ByteBuf byteBuf, final byte @NotNull [] bytes) {
|
||||
checkState(bytes.length < Short.MAX_VALUE, "Too long array");
|
||||
writeVarInt(byteBuf, bytes.length);
|
||||
byteBuf.writeBytes(bytes);
|
||||
@@ -289,27 +304,6 @@ public class ProtocolUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L276
|
||||
public <E extends Enum<E>> void writeEnumSet(final ByteBuf byteBuf,
|
||||
final EnumSet<E> enumset,
|
||||
final @NotNull Class<E> oclass) {
|
||||
final E[] enums = oclass.getEnumConstants();
|
||||
final BitSet bits = new BitSet(enums.length);
|
||||
|
||||
for (int i = 0; i < enums.length; ++i) {
|
||||
bits.set(i, enumset.contains(enums[i]));
|
||||
}
|
||||
|
||||
writeFixedBitSet(byteBuf, bits, enums.length);
|
||||
}
|
||||
|
||||
private static void writeFixedBitSet(final ByteBuf byteBuf, final @NotNull BitSet bits, final int size) {
|
||||
if (bits.length() > size) {
|
||||
throw new StackOverflowError("BitSet too large (expected " + size + " got " + bits.size() + ")");
|
||||
}
|
||||
byteBuf.writeBytes(Arrays.copyOf(bits.toByteArray(), (size + 8) >> 3));
|
||||
}
|
||||
|
||||
public static void writeCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
|
||||
|
||||
@@ -91,10 +91,6 @@ verification:
|
||||
collision:
|
||||
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
|
||||
enabled: true
|
||||
# Jaký typ bloku by měl Sonar použít pro kolizní platformu?
|
||||
# Typy: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# Toto byste měli upravovat, pouze pokud víte, co děláte
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Kontroluje, zda klient odesílá správné pakety při montáži vozidla
|
||||
vehicle:
|
||||
@@ -140,8 +136,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex pro validaci "client locale" při verifikaci
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Maximální počet login packetů, který musí hřáč poslat aby byl vyhozen
|
||||
max-login-packets: 256
|
||||
|
||||
# Je-li povoleno, hráč bude přenesen zpět na původní server
|
||||
# po úspěšném absolvování ověření bota.
|
||||
|
||||
@@ -88,10 +88,6 @@ verification:
|
||||
collision:
|
||||
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
|
||||
enabled: true
|
||||
# Welche Art von Block sollte Sonar für die Kollisionsplattform verwenden?
|
||||
# Mögliche Typen: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# Sie sollten dies nur bearbeiten, wenn Sie wissen, was Sie tun
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Überprüft, ob der Spieler beim Einsteigen in ein Boot die richtigen Pakete sendet
|
||||
vehicle:
|
||||
@@ -137,8 +133,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex zur Validierung der Client-Spracheinstellung während der Überprüfung
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Maximale Anzahl an Login-Paketen, die der Spieler senden muss, um gekickt zu werden
|
||||
max-login-packets: 256
|
||||
|
||||
# Wenn aktiviert, wird der Player zurück zum Ursprungsserver gesendet,
|
||||
# falls dieser die der Bot-Verifizierung erfolgreich besteht.
|
||||
|
||||
@@ -90,10 +90,6 @@ verification:
|
||||
collision:
|
||||
# Should Sonar check if the player collides with blocks? (Recommended)
|
||||
enabled: true
|
||||
# Which type of block should Sonar use for the collision platform?
|
||||
# Possible types: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# You should only edit this if you know what you're doing
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Checks if the client sends proper packets when entering a boat
|
||||
vehicle:
|
||||
@@ -140,8 +136,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex for validating client locale during verification
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# The number of login packets the player has to send in order to be kicked
|
||||
max-login-packets: 256
|
||||
|
||||
# If enabled, the player will be transferred back to the origin server
|
||||
# after successfully passing the bot verification.
|
||||
|
||||
@@ -90,10 +90,6 @@ verification:
|
||||
collision:
|
||||
# Sonar devrait-il vérifier si le joueur entre en collision avec des blocs ? (Recommandé)
|
||||
enabled: true
|
||||
# Quel type de bloc Sonar doit-il utiliser pour la plate-forme de collision ?
|
||||
# Types possibles : STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# Vous ne devriez modifier cela que si vous savez ce que vous faites
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Vérifie si le client envoie les paquets appropriés lorsqu'il monte dans un véhicule
|
||||
vehicle:
|
||||
@@ -139,8 +135,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex pour valider la locale client lors de la vérification
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Nombre maximal de paquets de connexion que le joueur doit envoyer pour être expulsé
|
||||
max-login-packets: 256
|
||||
|
||||
# S'il est activé, le joueur sera renvoyé au serveur d'origine
|
||||
# après avoir passé avec succès la vérification du bot.
|
||||
|
||||
@@ -90,10 +90,6 @@ verification:
|
||||
collision:
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს? (რეკომენდებულია)
|
||||
enabled: true
|
||||
# Which type of block should Sonar use for the collision platform?
|
||||
# შესაძლო ტიპები: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# თქვენ უნდა შეცვალოთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# ამოწმებს, აგზავნის თუ არა კლიენტი შესაბამის პაკეტებს სატრანსპორტო საშუალებაზე დაჯდომის დროს
|
||||
vehicle:
|
||||
@@ -139,8 +135,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex-ი კლიენტის ადგილის დასადასტურებლად ვერიფიკაციის დროს
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# მაქსიმალური რაოდენობა შესვლის (LOGIN) პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გავარდება სერვერიდან
|
||||
max-login-packets: 256
|
||||
|
||||
# თუ ჩართულია, მოთამაშე გადავა საწყის სერვერში ვერიფიკაციის წარმატებით გავლის შემდეგ
|
||||
# ეს ფუნქცია დაემატა Mojang-ის მიერ Minecraft-ის 1.20.5 ვერსიაში
|
||||
|
||||
@@ -90,10 +90,6 @@ verification:
|
||||
collision:
|
||||
# Moet Sonar controleren of de speler tegen blokken botst? (Aanbevolen)
|
||||
enabled: true
|
||||
# Welk type blok moet Sonar gebruiken voor het platform?
|
||||
# Mogelijke typen: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# Je moet dit alleen bewerken als je weet wat je doet
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Controleert of de klant de juiste pakketten verzendt bij het monteren van een voertuig
|
||||
vehicle:
|
||||
@@ -139,8 +135,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex voor het valideren van de landinstelling van de client tijdens verificatie
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Maximaal aantal loginpakketten dat de speler moet verzenden om te worden geschopt
|
||||
max-login-packets: 256
|
||||
|
||||
# Indien ingeschakeld, wordt de speler teruggestuurd naar de oorspronkelijke server
|
||||
# nadat u de bot-verificatie met succes heeft doorstaan.
|
||||
|
||||
@@ -90,10 +90,6 @@ verification:
|
||||
collision:
|
||||
# Czy Sonar powinien sprawdzać, czy gracz zderza się z blokami? (Zalecane)
|
||||
enabled: true
|
||||
# Jakiego typu bloku powinien użyć Sonar dla platformy kolizyjnej?
|
||||
# Możliwe rodzaje: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# Powinieneś to edytować tylko wtedy, gdy wiesz, co robisz
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Sprawdza, czy gracz wysyła odpowiednie pakiety podczas umieszczania na pojeździe.
|
||||
vehicle:
|
||||
@@ -139,8 +135,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex do sprawdzania ustawień regionalnych klienta podczas weryfikacji
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Liczba pakietów logowania, które gracz musi wysłać, aby zostać wyrzuconym.
|
||||
max-login-packets: 256
|
||||
|
||||
# Jeśli opcja ta jest włączona, gracz zostanie przeniesiony z powrotem
|
||||
# na serwer początkowy po pomyślnym przejściu weryfikacji antybotowej.
|
||||
|
||||
@@ -90,10 +90,6 @@ verification:
|
||||
collision:
|
||||
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
|
||||
enabled: true
|
||||
# Какой тип блока следует использовать в Sonar для платформы столкновений?
|
||||
# Возможные типы: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# Рекомендуется редактировать только если вы знаете, что делаете
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# Проверяет, отправляет ли клиент правильные пакеты при садке на транспортное средство
|
||||
vehicle:
|
||||
@@ -138,8 +134,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex для проверки локализации клиента при верификации
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Максимальное количество пакетов для входа на сервер, которое игрок должен отправить, чтобы быть выгнанным
|
||||
max-login-packets: 256
|
||||
|
||||
# Если включено, игрок будет возвращен на исходный сервер
|
||||
# после успешного прохождения проверки на бота.
|
||||
|
||||
@@ -88,10 +88,6 @@ verification:
|
||||
collision:
|
||||
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
|
||||
enabled: true
|
||||
# Sonar 应该使用哪种类型的块作为碰撞平台?
|
||||
# 可用类型: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
|
||||
# 只有当您知道您在做什么时, 才应该编辑它.
|
||||
collision-block-type: TRAPDOOR
|
||||
|
||||
# 检查客户端坐在实体上时是否发送了正确的数据包
|
||||
vehicle:
|
||||
@@ -136,8 +132,6 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# 用于检查客户端语言(例如 zh_CN)的正则表达式
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# 玩家最多能发送多少个数据包用于登录?
|
||||
max-login-packets: 256
|
||||
|
||||
# 开启后 玩家将在验证成功后被重定向到指定的服务器
|
||||
# 该功能由 Mojang 在 Minecraft 1.20.5 版本中引入
|
||||
|
||||
@@ -102,8 +102,6 @@ commands:
|
||||
remove: '<prefix>Úspěšně odstraněno <ip> z černé listiny.'
|
||||
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu
|
||||
add: '<prefix>Úspěšně přidáno <ip> na černou listinu.'
|
||||
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu, která je ověřená
|
||||
add-warning: '<prefix><red>Varování: <white><ip> je momentálně na bílé listině. Zvažte odstranění IP adresy ze seznamu ověřených hráčů, abyste se vyhnuli potenciálním problémům.'
|
||||
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu, ale již je na černé listině
|
||||
ip-duplicate: '<prefix>IP adresa, kterou jste zadali, je již na černé listině.'
|
||||
# Zpráva zobrazená, když někdo odstraní IP adresu z černé listiny, ale není na černé listině
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Zpráva zaznamenána do konzole, kdykoli se nový hráč připojí k serveru
|
||||
connection: '<username>/<ip> (<protocol>) se připojil.'
|
||||
# Zpráva zaznamenána do konzole, kdykoli hráč selže při ověření
|
||||
failed: '<ip> (<protocol>) neprošel kontrolou bota z důvodu: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) neprošel kontrolou bota z důvodu: <reason>'
|
||||
# Zpráva zaznamenána do konzole, kdykoli je hráč zařazen na černou listinu
|
||||
blacklisted: '<ip> (<protocol>) byl zařazen na černou listinu kvůli příliš mnoha neúspěšným pokusům'
|
||||
blacklisted: '<username>/<ip> (<protocol>) byl zařazen na černou listinu kvůli příliš mnoha neúspěšným pokusům'
|
||||
# Zpráva zaznamenána do konzole, kdykoli je hráč ověřen
|
||||
successful: '<username> byl úspěšně ověřen (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -101,8 +101,6 @@ commands:
|
||||
size: '<prefix>Die Blacklist enthält derzeit <amount> IP-Adresse(n).'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt
|
||||
add: '<prefix>Erfolgreich <ip> zur Blacklist hinzugefügt.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die verifiziert ist
|
||||
add-warning: '<prefix><red>Warnung: <white><ip> ist derzeit auf der Whitelist. Erwäge, die IP-Adresse aus der Liste der verifizierten Spieler zu entfernen, um potenzielle Probleme zu vermeiden.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse von der Blacklist entfernt
|
||||
remove: '<prefix>Erfolgreich <ip> von der Blacklist entfernt.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die bereits auf der Blacklist steht
|
||||
@@ -184,9 +182,9 @@ verification:
|
||||
# Nachricht, die im Protokoll angezeigt wird, wenn ein neuer Spieler den Server betretet
|
||||
connection: '<username>/<ip> (<protocol>) hat sich verbunden.'
|
||||
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler die Verifizierung nicht besteht
|
||||
failed: '<ip> (<protocol>) hat den Bot-Check nicht bestanden wegen: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) hat den Bot-Check nicht bestanden wegen: <reason>'
|
||||
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt wird
|
||||
blacklisted: '<ip> (<protocol>) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
|
||||
blacklisted: '<username>/<ip> (<protocol>) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
|
||||
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler erfolgreich verifiziert wurde
|
||||
successful: '<username> wurde erfolgreich verifiziert (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ commands:
|
||||
remove: '<prefix>Successfully removed <ip> from the blacklist.'
|
||||
# Message shown when someone adds an IP address to the blacklist
|
||||
add: '<prefix>Successfully added <ip> to the blacklist.'
|
||||
# Message shown when someone adds an IP address to the blacklist that is verified
|
||||
add-warning: '<prefix><red>Warning: <white><ip> is currently whitelisted. Consider removing the IP address from the list of verified players to avoid potential issues.'
|
||||
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
|
||||
ip-duplicate: '<prefix>The IP address you provided is already blacklisted.'
|
||||
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Message logged to console whenever a new player joins the server
|
||||
connection: '<username>/<ip> (<protocol>) has connected.'
|
||||
# Message logged to console whenever a player fails verification
|
||||
failed: '<ip> (<protocol>) has failed the bot check for: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) has failed the bot check for: <reason>'
|
||||
# Message logged to console whenever a player is blacklisted
|
||||
blacklisted: '<ip> (<protocol>) was blacklisted for too many failed attempts'
|
||||
blacklisted: '<username>/<ip> (<protocol>) was blacklisted for too many failed attempts'
|
||||
# Message logged to console whenever a player is verified
|
||||
successful: '<username> has been verified successfully (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ commands:
|
||||
remove: '<prefix>Se eliminó con éxito <ip> de la lista negra.'
|
||||
# Message shown when someone adds an IP address to the blacklist
|
||||
add: '<prefix>Añadida con éxito <ip> a la lista negra.'
|
||||
# Message shown when someone adds an IP address to the blacklist that is verified
|
||||
add-warning: '<prefix><red>Advertencia: <white><ip> Actualmente está en la lista blanca. Considere eliminar la dirección IP de la lista de jugadores verificados para evitar posibles problemas.'
|
||||
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
|
||||
ip-duplicate: '<prefix>La dirección IP que proporcionó ya está en la lista negra.'
|
||||
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Message logged to console whenever a new player joins the server
|
||||
connection: '<username>/<ip> (<protocol>) se ha conectado.'
|
||||
# Message logged to console whenever a player fails verification
|
||||
failed: '<ip> (<protocol>) no ha pasado la verificación de bot por: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) no ha pasado la verificación de bot por: <reason>'
|
||||
# Message logged to console whenever a player is blacklisted
|
||||
blacklisted: '<ip> (<protocol>) fue incluido en la lista negra por demasiados intentos fallidos.'
|
||||
blacklisted: '<username>/<ip> (<protocol>) fue incluido en la lista negra por demasiados intentos fallidos.'
|
||||
# Message logged to console whenever a player is verified
|
||||
successful: '<username> ha sido verificado exitosamente. (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -100,8 +100,6 @@ commands:
|
||||
size: '<prefix>La liste noire contient actuellement <amount> adresse(s) IP.'
|
||||
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire
|
||||
add: '<prefix>Adresse IP <ip> ajoutée avec succès à la liste noire.'
|
||||
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire qui est vérifiée
|
||||
add-warning: "<prefix><red>Avertissement : <white><ip> est actuellement sur liste blanche. Considérez la suppression de l'adresse IP de la liste des joueurs vérifiés pour éviter des problèmes potentiels."
|
||||
# Message affiché lorsque quelqu'un supprime une adresse IP de la liste noire
|
||||
remove: '<prefix>Adresse IP <ip> supprimée avec succès de la liste noire.'
|
||||
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire, mais qu'elle est déjà en liste noire
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Message enregistré dans la console chaque fois qu'un nouveau joueur rejoint le serveur
|
||||
connection: "<username>/<ip> (<protocol>) s'est connecté."
|
||||
# Message enregistré dans la console chaque fois qu'un joueur échoue à la vérification
|
||||
failed: '<ip> (<protocol>) a échoué au test du bot pour : <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) a échoué au test du bot pour : <reason>'
|
||||
# Message enregistré dans la console chaque fois qu'un joueur est sur liste noire
|
||||
blacklisted: '<ip> (<protocol>) a été mis sur liste noire pour trop de tentatives échouées'
|
||||
blacklisted: '<username>/<ip> (<protocol>) a été mis sur liste noire pour trop de tentatives échouées'
|
||||
# Message enregistré dans la console chaque fois qu'un joueur est vérifié
|
||||
successful: '<username> a été vérifié avec succès (<time-taken>s !).'
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ commands:
|
||||
remove: '<prefix>Berhasil menghapus <ip> dari daftar hitam.'
|
||||
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam
|
||||
add: '<prefix>Berhasil menambahkan <ip> ke daftar hitam.'
|
||||
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam yang diverifikasi
|
||||
add-warning: '<prefix><red>Peringatan: <white><ip> saat ini di daftar putih. Pertimbangkan untuk menghapus alamat IP dari daftar pemain yang diverifikasi untuk menghindari masalah potensial.'
|
||||
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam, tetapi sudah masuk daftar hitam
|
||||
ip-duplicate: '<prefix>Alamat IP yang Anda berikan sudah masuk daftar hitam.'
|
||||
# Pesan yang ditampilkan saat seseorang menghapus alamat IP dari daftar hitam, tetapi tidak masuk daftar hitam
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Pesan yang dicatat ke konsol setiap kali pemain baru bergabung ke server
|
||||
connection: '<username>/<ip> (<protocol>) telah terhubung.'
|
||||
# Pesan yang dicatat ke konsol setiap kali pemain gagal verifikasi
|
||||
failed: '<ip> (<protocol>) telah gagal dalam pemeriksaan bot untuk: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) telah gagal dalam pemeriksaan bot untuk: <reason>'
|
||||
# Pesan yang dicatat ke konsol setiap kali pemain masuk daftar hitam
|
||||
blacklisted: '<ip> (<protocol>) masuk daftar hitam karena terlalu banyak percobaan yang gagal'
|
||||
blacklisted: '<username>/<ip> (<protocol>) masuk daftar hitam karena terlalu banyak percobaan yang gagal'
|
||||
# Pesan yang dicatat ke konsol setiap kali pemain berhasil diverifikasi
|
||||
successful: '<username> berhasil diverifikasi dalam waktu (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -102,8 +102,6 @@ commands:
|
||||
remove: '<prefix>თქვენ წარმატებით მოაშორეთ <ip> შავი ნუსხიდან.'
|
||||
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში.
|
||||
add: '<prefix>თქვენ წარმატებით დაამატეთ <ip> შავ ნუსხაში.'
|
||||
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში მაგრამ ეს მისამართი ვერიფიცირებულია
|
||||
add-warning: '<prefix><red>გაფრთხილება: <white><ip> ამჟამად არის თეთრ ნუსხაში. გთხოვთ მოაშოროთ ეს IP მისამართი თეთრი სიიდან რათა აიცილოთ თავიდან პოტენციური პრობლემები.'
|
||||
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში მაგრამ იგი უკვე არის იქ.
|
||||
ip-duplicate: '<prefix>ეს IP მისამართი ისედაც შავ ნუსხაშია.'
|
||||
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე მოაშორებს IP მისამართს შავი ნუსხიდან მაგრამ იგი ისედაც არ არის იქ.
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# მესიჯი ჩაწერილი კონსოლში, როცა ახალი მოთამაშე შემოდის
|
||||
connection: '<username>/<ip> (<protocol>) დაკავშირდა.'
|
||||
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე წარუმატებლად ასრულებს შემოწმებას
|
||||
failed: '<ip> (<protocol>) წარუმატებლად გაირა შემოწმება: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) წარუმატებლად გაირა შემოწმება: <reason>'
|
||||
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შავ ნუსხაშია
|
||||
blacklisted: '<ip> (<protocol>) ბევრი წარუმატებელი შემოწმების გამო მოთამაშე შავ ნუსხაში შევიდა'
|
||||
blacklisted: '<username>/<ip> (<protocol>) ბევრი წარუმატებელი შემოწმების გამო მოთამაშე შავ ნუსხაში შევიდა'
|
||||
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შემოწმდა წარმატებულად
|
||||
successful: '<username> წარმატებულად შემოწმდა (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -100,8 +100,6 @@ commands:
|
||||
size: '<prefix>De zwarte lijst bevat momenteel <amount> IP-adres(sen).'
|
||||
# Bericht dat wordt getoond wanneer iemand een IP-adres aan de zwarte lijst toevoegt
|
||||
add: '<prefix><ip> is succesvol toegevoegd aan de zwarte lijst.'
|
||||
# Bericht dat wordt getoond wanneer iemand een IP-adres toevoegt aan de zwarte lijst die is geverifieerd
|
||||
add-warning: '<prefix><red>Attentie: <white><ip> staat momenteel op de witte lijst. Overwegen het verwijderen van het IP-adres uit de lijst met geverifieerde spelers om potentieel te voorkomen problemen.'
|
||||
# Bericht dat verschijnt wanneer iemand een IP-adres van de zwarte lijst verwijdert
|
||||
remove: '<prefix><ip> is met succes verwijderd van de zwarte lijst.'
|
||||
# Bericht dat wordt weergegeven wanneer iemand een IP-adres aan de zwarte lijst toevoegt, maar dit al op de zwarte lijst staat
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Bericht dat in de console wordt geregistreerd wanneer een nieuwe speler zich bij de server voegt
|
||||
connection: '<username>/<ip> (<protocol>) heeft verbinding gemaakt.'
|
||||
# Bericht dat in de console wordt geregistreerd wanneer de verificatie van een speler mislukt
|
||||
failed: '<ip> (<protocol>) heeft de bot-controle niet doorstaan vanwege: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) heeft de bot-controle niet doorstaan vanwege: <reason>'
|
||||
# Bericht dat op de console wordt geregistreerd wanneer een speler op de zwarte lijst staat
|
||||
blacklisted: '<ip> (<protocol>) stond op de zwarte lijst vanwege te veel mislukte pogingen'
|
||||
blacklisted: '<username>/<ip> (<protocol>) stond op de zwarte lijst vanwege te veel mislukte pogingen'
|
||||
# Bericht dat op de console wordt geregistreerd wanneer een speler is geverifieerd
|
||||
successful: '<username> is succesvol geverifieerd (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -105,8 +105,6 @@ commands:
|
||||
remove: <prefix>Pomyślnie usunięto <ip> z czarnej listy.
|
||||
# Message shown when someone adds an IP address to the blacklist
|
||||
add: <prefix>Pomyślnie dodano <ip> do czarnej listy.
|
||||
# Message shown when someone adds an IP address to the blacklist that is verified
|
||||
add-warning: '<prefix><red>Ostrzeżenie: <white><ip> znajduje się obecnie na białej liście. Rozważ usunięcie adresu IP z listy zweryfikowanych graczy, aby uniknąć potencjalnych problemów.'
|
||||
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
|
||||
ip-duplicate: <prefix>Podany adres IP znajduje się już na czarnej liście.
|
||||
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
|
||||
@@ -186,9 +184,9 @@ verification:
|
||||
# Message logged to console whenever a new player joins the server
|
||||
connection: '<username>/<ip> (<protocol>) połączył się.'
|
||||
# Message logged to console whenever a player fails verification
|
||||
failed: '<ip> (<protocol>) nie powiodło się sprawdzenie bota dla: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) nie powiodło się sprawdzenie bota dla: <reason>'
|
||||
# Message logged to console whenever a player is blacklisted
|
||||
blacklisted: '<ip> (<protocol>) został umieszczony na czarnej liście z powodu zbyt wielu nieudanych prób'
|
||||
blacklisted: '<username>/<ip> (<protocol>) został umieszczony na czarnej liście z powodu zbyt wielu nieudanych prób'
|
||||
# Message logged to console whenever a player is verified
|
||||
successful: '<username> został pomyślnie zweryfikowany (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -100,8 +100,6 @@ commands:
|
||||
size: '<prefix>В настоящее время черный список содержит <amount> IP-адрес(ов).'
|
||||
# Сообщение, которое показывается, когда кто-то добавляет IP-адрес в черный список
|
||||
add: '<prefix>Успешно добавлен <ip> в черный список.'
|
||||
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
|
||||
add-warning: '<prefix><red>Предупреждение: <white><ip> находится в белом списке. Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы избежать потенциальных ошибок.'
|
||||
# Сообщение, которое показывается, когда кто-то удаляет IP-адрес из черного списка
|
||||
remove: '<prefix>Успешно удален <ip> из черного списка.'
|
||||
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, но он уже находится в черном списке
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Сообщение, которое показыватся когда на сервер заходит новый игрок
|
||||
connection: '<username>/<ip> (<protocol>) подключился.'
|
||||
# Сообщение, которое показывается когда кто-то не прошел проверку на бота
|
||||
failed: '<ip> (<protocol>) не прошел проверку бота на: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) не прошел проверку бота на: <reason>'
|
||||
# Сообщение, которое показывается когда кто-то было занесен в черный список
|
||||
blacklisted: '<ip> (<protocol>) был занесен в черный список за слишком большое количество неудачных попыток'
|
||||
blacklisted: '<username>/<ip> (<protocol>) был занесен в черный список за слишком большое количество неудачных попыток'
|
||||
# Сообщение, которое выводится в консоль при успешной проверке игрока
|
||||
successful: '<username> прошел проверку (<time-taken>s!).'
|
||||
|
||||
|
||||
@@ -100,8 +100,6 @@ commands:
|
||||
size: '<prefix>Kara liste şu anda <amount> IP adres(ler)i içeriyor.'
|
||||
# Birisi bir IP adresini kara listeye eklediğinde gösterilen mesaj
|
||||
add: '<prefix>Başarıyla <ip> kara listeye eklendi.'
|
||||
# Birisi doğrulanmış olarak işaretlenmiş bir IP adresini kara listeye eklediğinde gösterilen mesaj
|
||||
add-warning: '<prefix><red>Uyarı: <white><ip> şu anda beyaz listede. Potansiyel sorunları önlemek için IP adresini doğrulanmış oyuncular listesinden kaldırmayı düşünün.'
|
||||
# Birisi bir IP adresini kara listeden çıkardığında gösterilen mesaj
|
||||
remove: '<prefix>Başarıyla <ip> kara listeden çıkarıldı.'
|
||||
# Birisi bir IP adresini kara listeye eklemeye çalıştığında ama zaten kara listede olduğunda gösterilen mesaj
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# Sunucuya yeni bir oyuncu katıldığında konsola kaydedilen mesaj
|
||||
connection: '<username>/<ip> (<protocol>) sunucuya bağlandı.'
|
||||
# Bir oyuncu doğrulamada başarısız olduğunda konsola kaydedilen mesaj
|
||||
failed: '<ip> (<protocol>) bot korumasında başarısız oldu: <reason>'
|
||||
failed: '<username>/<ip> (<protocol>) bot korumasında başarısız oldu: <reason>'
|
||||
# Bir oyuncu kara listeye alındığında konsola kaydedilen mesaj
|
||||
blacklisted: '<ip> (<protocol>) çok fazla doğrulama denemesinden dolayı kara listeye alındı'
|
||||
blacklisted: '<username>/<ip> (<protocol>) çok fazla doğrulama denemesinden dolayı kara listeye alındı'
|
||||
# Bir oyuncu doğrulandığında konsola kaydedilen mesaj
|
||||
successful: '<username> başarıyla doğrulandı! (<time-taken>s).'
|
||||
|
||||
|
||||
@@ -100,8 +100,6 @@ commands:
|
||||
size: '<prefix>黑名单中目前有 <amount> 个 IP 地址.'
|
||||
# 当手动将一个 IP 地址添加到黑名单时显示的消息
|
||||
add: '<prefix>成功将 IP 地址 <ip> 列入黑名单.'
|
||||
# 当尝试将白名单中的 IP 地址添加到黑名单中时显示的警告消息
|
||||
add-warning: '<prefix><red>警告: <white>IP 地址 <ip> 在白名单中. 添加到黑名单则意味着它将被同时从白名单中移除.'
|
||||
# 当尝试从黑名单中移除一个 IP 地址时显示的消息
|
||||
remove: '<prefix>成功将 IP 地址 <ip> 移出黑名单.'
|
||||
# 当尝试向黑名单添加一个已经在黑名单中的 IP 地址时显示的消息
|
||||
@@ -183,9 +181,9 @@ verification:
|
||||
# 当玩家进入服务器并验证时显示的消息
|
||||
connection: '<username>/<ip> (<protocol>) 连接到了服务器.'
|
||||
# 当玩家未通过检查时打印的消息
|
||||
failed: '<ip> (<protocol>) 由于原因 <reason> 无法通过检查.'
|
||||
failed: '<username>/<ip> (<protocol>) 由于原因 <reason> 无法通过检查.'
|
||||
# 当玩家被自动列入黑名单时打印的消息
|
||||
blacklisted: '<ip> (<protocol>) 因太多次无法完成验证而被列入黑名单.'
|
||||
blacklisted: '<username>/<ip> (<protocol>) 因太多次无法完成验证而被列入黑名单.'
|
||||
# 当玩家完成验证时打印的消息
|
||||
successful: '<username> 成功通过了验证 (耗时 <time-taken> 秒!).'
|
||||
|
||||
|
||||
Reference in new issue
Block a user