diff --git a/.gitignore b/.gitignore index 1bf8c14..af1ccbe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.idea/ /runClient/ /runServer/ +Command.bat diff --git a/src/main/java/com/diffusehyperion/inertiaanticheat/common/InertiaAntiCheat.java b/src/main/java/com/diffusehyperion/inertiaanticheat/common/InertiaAntiCheat.java index 2312cc6..50f258b 100644 --- a/src/main/java/com/diffusehyperion/inertiaanticheat/common/InertiaAntiCheat.java +++ b/src/main/java/com/diffusehyperion/inertiaanticheat/common/InertiaAntiCheat.java @@ -11,13 +11,22 @@ import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import static com.diffusehyperion.inertiaanticheat.common.util.InertiaAntiCheatConstants.MODLOGGER; @@ -62,8 +71,49 @@ public static Toml initializeConfig(String defaultConfigPath, Long currentConfig } } Toml config = new Toml().read(configFile); - if (!Objects.equals(config.getLong("debug.version", 0L), currentConfigVersion)) { - warn("Looks like your config file is outdated! Backing up current config, then creating an updated config."); + boolean versionMismatch = !Objects.equals(config.getLong("debug.version", 0L), currentConfigVersion); + boolean patched = patchTomlConfig(configFile, defaultConfigPath, currentConfigVersion, versionMismatch); + if (patched) { + config = new Toml().read(configFile); + info("Done! Your config was patched with new defaults (existing values preserved)."); + } + + return config; + } + + private static boolean patchTomlConfig(File configFile, String defaultConfigPath, + Long currentConfigVersion, boolean versionMismatch) { + List existingLines; + List defaultLines; + try { + existingLines = Files.readAllLines(configFile.toPath(), StandardCharsets.UTF_8); + defaultLines = readDefaultConfigLines(defaultConfigPath); + } catch (IOException e) { + throw new RuntimeException("Couldn't read config file!", e); + } + + ExistingToml existing = parseExistingToml(existingLines); + List templateSections = parseTemplateSections(defaultLines); + List additions = buildMissingBlocks(existing, templateSections, currentConfigVersion); + + boolean modified = false; + if (!additions.isEmpty()) { + if (!existingLines.isEmpty() && !existingLines.get(existingLines.size() - 1).isBlank()) { + existingLines.add(""); + } + existingLines.addAll(additions); + modified = true; + } + + boolean versionUpdated = updateDebugVersion(existingLines, currentConfigVersion); + modified |= versionUpdated; + + if (!modified) { + return false; + } + + if (versionMismatch) { + warn("Looks like your config file is outdated! Backing up current config, then patching it."); warn("Your config file will be backed up to \"BACKUP-InertiaAntiCheat.toml\"."); File backupFile = getConfigDir().resolve("BACKUP-InertiaAntiCheat.toml").toFile(); try { @@ -71,18 +121,242 @@ public static Toml initializeConfig(String defaultConfigPath, Long currentConfig } catch (IOException e) { throw new RuntimeException("Couldn't copy existing config file into a backup config file! Please do it manually.", e); } - if (!configFile.delete()) { - throw new RuntimeException("Couldn't delete config file! Please delete it manually."); + } + + try { + Files.write(configFile.toPath(), existingLines, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Couldn't write updated config file!", e); + } + + return true; + } + + private static List readDefaultConfigLines(String defaultConfigPath) throws IOException { + try (InputStream stream = InertiaAntiCheatServer.class.getResourceAsStream(defaultConfigPath)) { + if (stream == null) { + throw new RuntimeException("Default config resource not found: " + defaultConfigPath); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8).lines().toList(); + } + } + + private static ExistingToml parseExistingToml(List lines) { + ExistingToml existing = new ExistingToml(); + String currentSection = ""; + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("//")) { + continue; } - try { - Files.copy(Objects.requireNonNull(InertiaAntiCheatServer.class.getResourceAsStream(defaultConfigPath)), configFile.toPath()); - } catch (IOException e) { - throw new RuntimeException("Couldn't create a default config!", e); + + if (isSectionHeader(trimmed)) { + currentSection = trimmed.substring(1, trimmed.length() - 1).trim(); + existing.sections.add(currentSection); + continue; + } + + int equalsIndex = trimmed.indexOf('='); + if (equalsIndex < 0) { + continue; } - config = new Toml().read(configFile); // update config to new file - info("Done! Please readjust the configs in the new file accordingly."); + + String key = trimmed.substring(0, equalsIndex).trim(); + existing.keysBySection.computeIfAbsent(currentSection, ignored -> new HashSet<>()).add(key); + } + return existing; + } + + private static List parseTemplateSections(List lines) { + List sections = new ArrayList<>(); + TomlTemplateSection current = new TomlTemplateSection(""); + sections.add(current); + List pending = new ArrayList<>(); + + for (String line : lines) { + String trimmed = line.trim(); + if (isSectionHeader(trimmed)) { + String sectionName = trimmed.substring(1, trimmed.length() - 1).trim(); + current = new TomlTemplateSection(sectionName); + current.headerComments.addAll(pending); + pending.clear(); + sections.add(current); + continue; + } + + if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("//")) { + pending.add(line); + continue; + } + + int equalsIndex = trimmed.indexOf('='); + if (equalsIndex < 0) { + pending.add(line); + continue; + } + + String key = trimmed.substring(0, equalsIndex).trim(); + List block = new ArrayList<>(pending); + pending.clear(); + block.add(line); + current.keyBlocks.putIfAbsent(key, block); + } + + return sections; + } + + private static List buildMissingBlocks(ExistingToml existing, List sections, + Long currentConfigVersion) { + List additions = new ArrayList<>(); + for (TomlTemplateSection section : sections) { + String sectionName = section.name; + boolean sectionExists = sectionName.isEmpty() || existing.sections.contains(sectionName); + + if (!sectionExists) { + appendSectionBlock(additions, section, currentConfigVersion); + continue; + } + + Set existingKeys = existing.keysBySection.getOrDefault(sectionName, Set.of()); + List missingBlocks = new ArrayList<>(); + for (Map.Entry> entry : section.keyBlocks.entrySet()) { + String key = entry.getKey(); + if (existingKeys.contains(key)) { + continue; + } + List block = new ArrayList<>(entry.getValue()); + if ("debug".equals(sectionName) && "version".equals(key)) { + replaceVersionInBlock(block, currentConfigVersion); + } + missingBlocks.addAll(block); + } + + if (!missingBlocks.isEmpty()) { + if (!additions.isEmpty() && !additions.get(additions.size() - 1).isBlank()) { + additions.add(""); + } + if (!sectionName.isEmpty()) { + additions.add("[" + sectionName + "]"); + } + additions.addAll(missingBlocks); + } + } + return additions; + } + + private static void appendSectionBlock(List additions, TomlTemplateSection section, + Long currentConfigVersion) { + if (!additions.isEmpty() && !additions.get(additions.size() - 1).isBlank()) { + additions.add(""); + } + additions.addAll(section.headerComments); + if (!section.name.isEmpty()) { + additions.add("[" + section.name + "]"); + } + for (Map.Entry> entry : section.keyBlocks.entrySet()) { + List block = new ArrayList<>(entry.getValue()); + if ("debug".equals(section.name) && "version".equals(entry.getKey())) { + replaceVersionInBlock(block, currentConfigVersion); + } + additions.addAll(block); + } + } + + private static boolean updateDebugVersion(List lines, Long currentConfigVersion) { + String currentSection = ""; + boolean updated = false; + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("//")) { + continue; + } + if (isSectionHeader(trimmed)) { + currentSection = trimmed.substring(1, trimmed.length() - 1).trim(); + continue; + } + int equalsIndex = trimmed.indexOf('='); + if (equalsIndex < 0) { + continue; + } + String key = trimmed.substring(0, equalsIndex).trim(); + if ("debug".equals(currentSection) && "version".equals(key)) { + String newLine = replaceTomlValue(line, String.valueOf(currentConfigVersion)); + if (!newLine.equals(line)) { + lines.set(i, newLine); + updated = true; + } + } + } + return updated; + } + + private static void replaceVersionInBlock(List block, Long currentConfigVersion) { + for (int i = 0; i < block.size(); i++) { + String line = block.get(i); + String trimmed = line.trim(); + if (trimmed.startsWith("version") && trimmed.contains("=")) { + block.set(i, replaceTomlValue(line, String.valueOf(currentConfigVersion))); + return; + } + } + } + + private static boolean isSectionHeader(String trimmed) { + return trimmed.startsWith("[") && trimmed.endsWith("]"); + } + + private static String replaceTomlValue(String line, String newValue) { + int equalsIndex = line.indexOf('='); + if (equalsIndex < 0) { + return line; + } + String prefix = line.substring(0, equalsIndex + 1); + String remainder = line.substring(equalsIndex + 1); + int commentIndex = findInlineCommentIndex(remainder); + String comment = commentIndex >= 0 ? remainder.substring(commentIndex).trim() : ""; + String suffix = comment.isEmpty() ? "" : " " + comment; + return prefix + " " + newValue + suffix; + } + + private static int findInlineCommentIndex(String text) { + boolean inQuotes = false; + char quoteChar = 0; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (inQuotes) { + if (c == '\\') { + i++; + } else if (c == quoteChar) { + inQuotes = false; + } + } else { + if (c == '"' || c == '\'') { + inQuotes = true; + quoteChar = c; + } else if (c == '#') { + return i; + } else if (c == '/' && i + 1 < text.length() && text.charAt(i + 1) == '/') { + return i; + } + } + } + return -1; + } + + private static final class ExistingToml { + private final Set sections = new HashSet<>(); + private final Map> keysBySection = new HashMap<>(); + } + + private static final class TomlTemplateSection { + private final String name; + private final List headerComments = new ArrayList<>(); + private final LinkedHashMap> keyBlocks = new LinkedHashMap<>(); + + private TomlTemplateSection(String name) { + this.name = name; } - return config; } public static Path getConfigDir() { diff --git a/src/main/java/com/diffusehyperion/inertiaanticheat/common/interfaces/UpgradedServerCommonNetworkHandler.java b/src/main/java/com/diffusehyperion/inertiaanticheat/common/interfaces/UpgradedServerCommonNetworkHandler.java new file mode 100644 index 0000000..60f76cc --- /dev/null +++ b/src/main/java/com/diffusehyperion/inertiaanticheat/common/interfaces/UpgradedServerCommonNetworkHandler.java @@ -0,0 +1,7 @@ +package com.diffusehyperion.inertiaanticheat.common.interfaces; + +import net.minecraft.network.ClientConnection; + +public interface UpgradedServerCommonNetworkHandler { + ClientConnection inertiaAntiCheat$getConnection(); +} diff --git a/src/main/java/com/diffusehyperion/inertiaanticheat/mixins/server/ServerCommonNetworkHandlerMixin.java b/src/main/java/com/diffusehyperion/inertiaanticheat/mixins/server/ServerCommonNetworkHandlerMixin.java new file mode 100644 index 0000000..56e7ee2 --- /dev/null +++ b/src/main/java/com/diffusehyperion/inertiaanticheat/mixins/server/ServerCommonNetworkHandlerMixin.java @@ -0,0 +1,19 @@ +package com.diffusehyperion.inertiaanticheat.mixins.server; + +import com.diffusehyperion.inertiaanticheat.common.interfaces.UpgradedServerCommonNetworkHandler; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.network.ServerCommonNetworkHandler; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ServerCommonNetworkHandler.class) +public abstract class ServerCommonNetworkHandlerMixin implements UpgradedServerCommonNetworkHandler { + @Shadow @Final + ClientConnection connection; + + @Override + public ClientConnection inertiaAntiCheat$getConnection() { + return this.connection; + } +} diff --git a/src/main/java/com/diffusehyperion/inertiaanticheat/server/FloodgateBridge.java b/src/main/java/com/diffusehyperion/inertiaanticheat/server/FloodgateBridge.java new file mode 100644 index 0000000..432c171 --- /dev/null +++ b/src/main/java/com/diffusehyperion/inertiaanticheat/server/FloodgateBridge.java @@ -0,0 +1,104 @@ +package com.diffusehyperion.inertiaanticheat.server; + +import java.lang.reflect.Method; +import java.util.UUID; + +final class FloodgateBridge { + private static final String API_CLASS_NAME = "org.geysermc.floodgate.api.FloodgateApi"; + private static final FloodgateBridge INSTANCE = new FloodgateBridge(); + + private final Object initLock = new Object(); + + private volatile boolean available; + private volatile Object apiInstance; + private volatile Method isFloodgateByUuid; + private volatile Method getPlayerByUuid; + + static FloodgateBridge get() { + return INSTANCE; + } + + private FloodgateBridge() { + } + + boolean isAvailable() { + return ensureAvailable(); + } + + boolean isFloodgatePlayer(UUID uuid) { + if (!ensureAvailable()) { + return false; + } + if (uuid != null && invokeBoolean(isFloodgateByUuid, uuid)) { + return true; + } + if (uuid != null && invokeGetPlayer(getPlayerByUuid, uuid)) { + return true; + } + return false; + } + + private boolean ensureAvailable() { + if (available) { + return true; + } + synchronized (initLock) { + if (available) { + return true; + } + tryInit(); + return available; + } + } + + private void tryInit() { + Object api = null; + Method isByUuid = null; + Method getByUuid = null; + + try { + Class apiClass = Class.forName(API_CLASS_NAME); + api = apiClass.getMethod("getInstance").invoke(null); + isByUuid = getMethod(apiClass, "isFloodgatePlayer", UUID.class); + getByUuid = getMethod(apiClass, "getPlayer", UUID.class); + } catch (Exception e) { + // ignore, we'll report unavailable + } + + this.apiInstance = api; + this.isFloodgateByUuid = isByUuid; + this.getPlayerByUuid = getByUuid; + this.available = api != null; + } + + private boolean invokeBoolean(Method method, Object arg) { + if (method == null) { + return false; + } + try { + Object result = method.invoke(apiInstance, arg); + return result instanceof Boolean && (Boolean) result; + } catch (Exception e) { + return false; + } + } + + private boolean invokeGetPlayer(Method method, Object arg) { + if (method == null) { + return false; + } + try { + return method.invoke(apiInstance, arg) != null; + } catch (Exception e) { + return false; + } + } + + private static Method getMethod(Class apiClass, String name, Class param) { + try { + return apiClass.getMethod(name, param); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/diffusehyperion/inertiaanticheat/server/GeyserBypassTracker.java b/src/main/java/com/diffusehyperion/inertiaanticheat/server/GeyserBypassTracker.java new file mode 100644 index 0000000..76ecfb1 --- /dev/null +++ b/src/main/java/com/diffusehyperion/inertiaanticheat/server/GeyserBypassTracker.java @@ -0,0 +1,101 @@ +package com.diffusehyperion.inertiaanticheat.server; + +import com.diffusehyperion.inertiaanticheat.common.interfaces.UpgradedServerCommonNetworkHandler; +import com.diffusehyperion.inertiaanticheat.common.interfaces.UpgradedServerLoginNetworkHandler; +import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginNetworkHandler; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static com.diffusehyperion.inertiaanticheat.server.InertiaAntiCheatServer.debugInfo; +import static com.diffusehyperion.inertiaanticheat.server.InertiaAntiCheatServer.debugLine; + +final class GeyserBypassTracker { + private static final Set PENDING_CONNECTIONS = ConcurrentHashMap.newKeySet(); + + private GeyserBypassTracker() { + } + + static void register() { + ServerPlayConnectionEvents.JOIN.register(GeyserBypassTracker::handleJoin); + ServerPlayConnectionEvents.DISCONNECT.register(GeyserBypassTracker::handleDisconnect); + ServerLoginConnectionEvents.DISCONNECT.register(GeyserBypassTracker::handleLoginDisconnect); + } + + static boolean markPending(ClientConnection connection) { + if (connection == null) { + return false; + } + return PENDING_CONNECTIONS.add(connection); + } + + private static void handleJoin(ServerPlayNetworkHandler handler, net.fabricmc.fabric.api.networking.v1.PacketSender sender, MinecraftServer server) { + if (!allowGeyserClients()) { + return; + } + ClientConnection connection = extractConnection(handler); + if (connection == null || !PENDING_CONNECTIONS.remove(connection)) { + return; + } + ServerPlayerEntity player = handler.player; + FloodgateBridge bridge = FloodgateBridge.get(); + boolean floodgate = bridge.isFloodgatePlayer(player.getUuid()); + if (!floodgate && !bridge.isAvailable()) { + debugInfo(player.getName().getString() + " joined while pending geyser validation, but Floodgate API is unavailable"); + disconnectVanilla(player); + return; + } + + if (!floodgate) { + debugInfo(player.getName().getString() + " was pending geyser validation but is not a Floodgate player"); + disconnectVanilla(player); + return; + } + + debugInfo(player.getName().getString() + " confirmed as Floodgate player after login"); + debugLine(); + } + + private static void handleDisconnect(ServerPlayNetworkHandler handler, MinecraftServer server) { + ClientConnection connection = extractConnection(handler); + if (connection != null) { + PENDING_CONNECTIONS.remove(connection); + } + } + + private static void handleLoginDisconnect(ServerLoginNetworkHandler handler, MinecraftServer server) { + if (!allowGeyserClients()) { + return; + } + UpgradedServerLoginNetworkHandler upgraded = (UpgradedServerLoginNetworkHandler) handler; + ClientConnection connection = upgraded.inertiaAntiCheat$getConnection(); + if (connection != null) { + PENDING_CONNECTIONS.remove(connection); + } + } + + private static boolean allowGeyserClients() { + return InertiaAntiCheatServer.serverConfig != null + && InertiaAntiCheatServer.serverConfig.getBoolean("geyser.allow_geyser_clients", false); + } + + private static void disconnectVanilla(ServerPlayerEntity player) { + player.networkHandler.disconnect(Text.of( + InertiaAntiCheatServer.serverConfig.getString("validation.vanillaKickMessage") + )); + } + + private static ClientConnection extractConnection(ServerPlayNetworkHandler handler) { + if (!(handler instanceof UpgradedServerCommonNetworkHandler upgraded)) { + return null; + } + return upgraded.inertiaAntiCheat$getConnection(); + } +} diff --git a/src/main/java/com/diffusehyperion/inertiaanticheat/server/ServerLoginModlistTransferHandler.java b/src/main/java/com/diffusehyperion/inertiaanticheat/server/ServerLoginModlistTransferHandler.java index b644f2e..0a0d97c 100644 --- a/src/main/java/com/diffusehyperion/inertiaanticheat/server/ServerLoginModlistTransferHandler.java +++ b/src/main/java/com/diffusehyperion/inertiaanticheat/server/ServerLoginModlistTransferHandler.java @@ -38,6 +38,7 @@ public class ServerLoginModlistTransferHandler { public static void init() { ServerLoginConnectionEvents.QUERY_START.register(ServerLoginModlistTransferHandler::initiateConnection); + GeyserBypassTracker.register(); } /** @@ -76,7 +77,32 @@ private static void initiateConnection(ServerLoginNetworkHandler handler, Minecr ServerLoginNetworking.LoginSynchronizer synchronizer, PacketSender packetSender) { LoginPacketSender sender = (LoginPacketSender) packetSender; + boolean allowGeyserClients = InertiaAntiCheatServer.serverConfig.getBoolean("geyser.allow_geyser_clients", false); + UpgradedServerLoginNetworkHandler upgradedHandler = (UpgradedServerLoginNetworkHandler) handler; + net.minecraft.network.ClientConnection connection = upgradedHandler.inertiaAntiCheat$getConnection(); + FloodgateBridge floodgateBridge = FloodgateBridge.get(); + com.mojang.authlib.GameProfile profile = upgradedHandler.inertiaAntiCheat$getGameProfile(); + java.util.UUID profileId = extractProfileId(profile); + boolean isFloodgate = allowGeyserClients + && floodgateBridge.isFloodgatePlayer(profileId); + if (isFloodgate) { + debugInfo(handler.getConnectionInfo() + " is a Geyser client and is allowed"); + this.loginBlocker.complete(null); + debugLine(); + return; + } + if (!b) { + // Client doesn't respond to mod messages (likely vanilla client) + if (allowGeyserClients) { + if (GeyserBypassTracker.markPending(connection)) { + debugInfo(handler.getConnectionInfo() + " does not respond to mod messages, deferring Geyser check until play stage"); + this.loginBlocker.complete(null); + debugLine(); + return; + } + debugInfo(handler.getConnectionInfo() + " does not respond to mod messages and could not be tracked for Geyser validation"); + } debugInfo(handler.getConnectionInfo() + " does not respond to mod messages, kicking now"); handler.disconnect(Text.of(InertiaAntiCheatServer.serverConfig.getString("validation.vanillaKickMessage"))); return; @@ -177,4 +203,39 @@ private static void initiateConnection(ServerLoginNetworkHandler handler, Minecr validatorAdaptor.future.whenComplete((ignored1, ignored2) -> this.loginBlocker.complete(null)); debugLine(); } + + private java.util.UUID extractProfileId(com.mojang.authlib.GameProfile profile) { + if (profile == null) { + return null; + } + try { + java.lang.reflect.Method method = profile.getClass().getMethod("getId"); + Object result = method.invoke(profile); + if (result instanceof java.util.UUID uuid) { + return uuid; + } + } catch (Exception ignored) { + // ignore + } + try { + java.lang.reflect.Method method = profile.getClass().getMethod("getUuid"); + Object result = method.invoke(profile); + if (result instanceof java.util.UUID uuid) { + return uuid; + } + } catch (Exception ignored) { + // ignore + } + try { + java.lang.reflect.Method method = profile.getClass().getMethod("getUUID"); + Object result = method.invoke(profile); + if (result instanceof java.util.UUID uuid) { + return uuid; + } + } catch (Exception ignored) { + // ignore + } + return null; + } + } diff --git a/src/main/resources/config/server/InertiaAntiCheat.toml b/src/main/resources/config/server/InertiaAntiCheat.toml index f4b4906..4065c97 100644 --- a/src/main/resources/config/server/InertiaAntiCheat.toml +++ b/src/main/resources/config/server/InertiaAntiCheat.toml @@ -69,6 +69,10 @@ whitelist = ["Whitelisted mods: ", "None"] # Setting this to be an empty list will cause the icon to not show up. hash = ["Requires modpack: ", "Modpack Name"] +[geyser] +# Whether to allow Geyser clients (Bedrock players) to join the server. (Floodgate is required) +allow_geyser_clients = false (Warning: Extra security risk, use at your own risk) + [debug] # Show additional information in server logs. debug = false diff --git a/src/main/resources/inertiaanticheat.mixins.json b/src/main/resources/inertiaanticheat.mixins.json index 579e1a3..3809f91 100644 --- a/src/main/resources/inertiaanticheat.mixins.json +++ b/src/main/resources/inertiaanticheat.mixins.json @@ -10,10 +10,11 @@ "requireAnnotations": true }, "server": [ + "server.ServerCommonNetworkHandlerMixin", "server.ServerLoginNetworkHandlerMixin", "server.ServerQueryNetworkHandlerMixin" ], "mixins": [ "common.QueryStatesMixin" ] -} \ No newline at end of file +}