From ba004a75228421b945e4505eca81e6a81fc5c446 Mon Sep 17 00:00:00 2001 From: Micle Date: Mon, 31 Oct 2022 20:07:14 +0000 Subject: [PATCH] Initial port of 1.18.2-3.0.1 to 1.19.2. --- .gitignore | 6 + LICENSE | 2 + build.gradle | 125 +++++++ gradle.properties | 12 + gradle/wrapper/gradle-wrapper.properties | 1 + settings.gradle | 1 + .../loginprotection/LoginProtection.java | 43 +++ .../loginprotection/data/ProtectedPlayer.java | 76 ++++ .../data/ProtectedPlayerManager.java | 234 ++++++++++++ .../client/OnClientInputEventHandler.java | 345 ++++++++++++++++++ .../OnLivingSetAttackTargetEventHandler.java | 32 ++ .../common/OnPlayerDamageEventHandler.java | 23 ++ .../common/OnPlayerJoinEventHandler.java | 17 + .../common/OnPlayerLeaveEventHandler.java | 17 + .../mixin/GuiRenderTickMixin.java | 50 +++ .../network/NetworkManager.java | 49 +++ .../network/client/InputPacket.java | 41 +++ .../network/client/LastInputTickPacket.java | 56 +++ .../network/server/PlayerStatePacket.java | 32 ++ .../server/RequestLastInputTickPacket.java | 30 ++ .../micle/loginprotection/proxy/IProxy.java | 11 + .../micle/loginprotection/proxy/Proxy.java | 145 ++++++++ .../micle/loginprotection/setup/Config.java | 174 +++++++++ src/main/resources/META-INF/mods.toml | 58 +++ .../resources/loginprotection.mixins.json | 15 + src/main/resources/logo.jpg | Bin 0 -> 31912 bytes src/main/resources/pack.mcmeta | 6 + 27 files changed, 1601 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 settings.gradle create mode 100644 src/main/java/dev/micle/loginprotection/LoginProtection.java create mode 100644 src/main/java/dev/micle/loginprotection/data/ProtectedPlayer.java create mode 100644 src/main/java/dev/micle/loginprotection/data/ProtectedPlayerManager.java create mode 100644 src/main/java/dev/micle/loginprotection/events/client/OnClientInputEventHandler.java create mode 100644 src/main/java/dev/micle/loginprotection/events/common/OnLivingSetAttackTargetEventHandler.java create mode 100644 src/main/java/dev/micle/loginprotection/events/common/OnPlayerDamageEventHandler.java create mode 100644 src/main/java/dev/micle/loginprotection/events/common/OnPlayerJoinEventHandler.java create mode 100644 src/main/java/dev/micle/loginprotection/events/common/OnPlayerLeaveEventHandler.java create mode 100644 src/main/java/dev/micle/loginprotection/mixin/GuiRenderTickMixin.java create mode 100644 src/main/java/dev/micle/loginprotection/network/NetworkManager.java create mode 100644 src/main/java/dev/micle/loginprotection/network/client/InputPacket.java create mode 100644 src/main/java/dev/micle/loginprotection/network/client/LastInputTickPacket.java create mode 100644 src/main/java/dev/micle/loginprotection/network/server/PlayerStatePacket.java create mode 100644 src/main/java/dev/micle/loginprotection/network/server/RequestLastInputTickPacket.java create mode 100644 src/main/java/dev/micle/loginprotection/proxy/IProxy.java create mode 100644 src/main/java/dev/micle/loginprotection/proxy/Proxy.java create mode 100644 src/main/java/dev/micle/loginprotection/setup/Config.java create mode 100644 src/main/resources/META-INF/mods.toml create mode 100644 src/main/resources/loginprotection.mixins.json create mode 100644 src/main/resources/logo.jpg create mode 100644 src/main/resources/pack.mcmeta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f83c7f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.gradle/ +/.idea/ +/build/ +/run/ +/src/test/ +/src/generated/resources/.cache/cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5739720 --- /dev/null +++ b/LICENSE @@ -0,0 +1,2 @@ +Copyright (c) 2022 Micle +All rights reserved. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d6ade27 --- /dev/null +++ b/build.gradle @@ -0,0 +1,125 @@ +buildscript { + repositories { + // These repositories are only for Gradle plugins, put any other repositories in the repository block further below + maven { url = 'https://maven.minecraftforge.net' } + maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } + mavenCentral() + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+', changing: true + classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT' + } +} + +apply plugin: 'net.minecraftforge.gradle' +apply plugin: 'org.spongepowered.mixin' + +def archiveVersion = "${project.mcVersion}-${project.buildVersion}" as Object + +java.toolchain.languageVersion = JavaLanguageVersion.of(17) + +minecraft { + mappings channel: 'official', version: mcVersion + + accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') + + runs { + client { + workingDirectory project.file('run') + + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + + arg "-mixin.config=${modID}.mixins.json" + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + + mods { + loginprotection { + source sourceSets.main + } + } + } + + server { + workingDirectory project.file('run') + + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + + arg "-mixin.config=${modID}.mixins.json" + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + + mods { + loginprotection { + source sourceSets.main + } + } + } + + data { + workingDirectory project.file('run') + + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + + arg "-mixin.config=${modID}.mixins.json" + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + + args '--mod', 'loginprotection', '--all', + '--existing', file('src/main/resources').toString(), + '--existing', file('src/generated/resources').toString(), + '--output', file('src/generated/resources/') + + mods { + loginprotection { + source sourceSets.main + } + } + } + } +} + +mixin { + add sourceSets.main, "${modID}.refmap.json" + config "${modID}.mixins.json" +} + +sourceSets.main.resources { + srcDir 'src/generated/resources' +} + +repositories { + maven { + name = "Progwml6 maven" + url = "https://dvs1.progwml6.com/files/maven/" + } + maven { + name = "ModMaven" + url = "https://modmaven.dev" + } +} + +dependencies { + minecraft "net.minecraftforge:forge:${project.mcVersion}-${project.forgeVersion}" + annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' +} + +jar { + archiveFileName = "${project.archivesBaseName}-${archiveVersion}.jar" + manifest { + attributes([ + "Specification-Title" : project.name, + "Specification-Vendor" : project.author, + "Specification-Version" : "1", + "Implementation-Title" : project.name, + "Implementation-Vendor" : project.author, + "Implementation-Version" : archiveVersion, + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") + ]) + } +} + +jar.finalizedBy('reobfJar') \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..31be383 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +group = dev.micle +archivesBaseName = micles-login-protection-forge +modID = loginprotection +name = Micle's Login Protection +author = Micle + +buildVersion = 3.0.0 +mcVersion = 1.19.2 +forgeVersion = 43.1.47 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..73bb918 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..46fbea0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'LoginProtection-1.19' diff --git a/src/main/java/dev/micle/loginprotection/LoginProtection.java b/src/main/java/dev/micle/loginprotection/LoginProtection.java new file mode 100644 index 0000000..3cbc3f3 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/LoginProtection.java @@ -0,0 +1,43 @@ +package dev.micle.loginprotection; + +import dev.micle.loginprotection.proxy.IProxy; +import dev.micle.loginprotection.proxy.Proxy; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.common.Mod; + +import java.util.Optional; + +@Mod(LoginProtection.MOD_ID) +public class LoginProtection { + public static final String MOD_ID = "loginprotection"; + private static IProxy proxy; + + public LoginProtection() { + proxy = DistExecutor.safeRunForDist( + () -> Proxy.Client::new, + () -> Proxy.Server::new + ); + } + + public static ResourceLocation createResourceLocation(String name) throws IllegalArgumentException { + if (name.contains(":")) { + throw new IllegalArgumentException("Name contains namespace!"); + } + return new ResourceLocation(MOD_ID, name); + } + + public static String getVersion() { + Optional modContainer = ModList.get().getModContainerById(MOD_ID); + if (modContainer.isPresent()) { + return modContainer.get().getModInfo().getVersion().toString(); + } + return "0.0.0"; + } + + public static IProxy getProxy() { + return proxy; + } +} diff --git a/src/main/java/dev/micle/loginprotection/data/ProtectedPlayer.java b/src/main/java/dev/micle/loginprotection/data/ProtectedPlayer.java new file mode 100644 index 0000000..d4e22fb --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/data/ProtectedPlayer.java @@ -0,0 +1,76 @@ +package dev.micle.loginprotection.data; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +public class ProtectedPlayer { + // Initialize variables + private final UUID playerUUID; + private final Timer timer = new Timer(); + private TimerTask afkTimerTask, gracePeriodTimerTask; + private State state; + + /** + * Constructor for a ProtectedPlayer. + * @param playerUUID UUID of player to use. + */ + public ProtectedPlayer(UUID playerUUID, State state) { + this.playerUUID = playerUUID; + this.state = state; + } + + /** + * @return UUID of player, + */ + public UUID getPlayerUUID() { + return playerUUID; + } + + public Timer getTimer() { + return timer; + } + + public void setGracePeriodTimerTask(TimerTask gracePeriodTimerTask, long delay) { + if (this.gracePeriodTimerTask != null) { + this.gracePeriodTimerTask.cancel(); + } + if (gracePeriodTimerTask != null) { + this.gracePeriodTimerTask = gracePeriodTimerTask; + timer.schedule(this.gracePeriodTimerTask, delay); + } + } + + public void setAfkTimerTask(TimerTask afkTimerTask, long delay) { + if (this.afkTimerTask != null) { + this.afkTimerTask.cancel(); + } + if (afkTimerTask != null) { + this.afkTimerTask = afkTimerTask; + timer.schedule(this.afkTimerTask, delay); + } + } + + /** + * @return Current state of the player. + */ + public State getState() { + return state; + } + + /** + * Set the state of the player. + * @param state Player's new state. + */ + public void setState(State state) { + this.state = state; + } + + public enum State { + JOINING, + AFK, + ACTIVE, + LOGIN_GRACE, + AFK_GRACE + } +} diff --git a/src/main/java/dev/micle/loginprotection/data/ProtectedPlayerManager.java b/src/main/java/dev/micle/loginprotection/data/ProtectedPlayerManager.java new file mode 100644 index 0000000..85f28da --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/data/ProtectedPlayerManager.java @@ -0,0 +1,234 @@ +package dev.micle.loginprotection.data; + +import dev.micle.loginprotection.LoginProtection; +import dev.micle.loginprotection.network.NetworkManager; +import dev.micle.loginprotection.network.server.PlayerStatePacket; +import dev.micle.loginprotection.network.server.RequestLastInputTickPacket; +import dev.micle.loginprotection.setup.Config; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.network.NetworkDirection; + +import java.util.ArrayList; +import java.util.List; +import java.util.TimerTask; +import java.util.UUID; + +public class ProtectedPlayerManager { + // Initialize variables + private static final List protectedPlayers = new ArrayList<>(); + + /** + * Method for initializing the ProtectedPlayerManager. + */ + public static void init() { + for (ProtectedPlayer protectedPlayer : protectedPlayers) { + removePlayer(protectedPlayer.getPlayerUUID()); + } + } + + /** + * Adds a player to be protected. + * @param playerUUID UUID of player to protect. + */ + public static void addPlayer(UUID playerUUID) { + if (getPlayer(playerUUID) != null) { + return; + } + try { + protectedPlayers.add(new ProtectedPlayer(playerUUID, ProtectedPlayer.State.JOINING)); + NetworkManager.getChannel().sendTo(new PlayerStatePacket(ProtectedPlayer.State.JOINING), + LoginProtection.getProxy().getServer().getPlayerList().getPlayer(playerUUID).connection.getConnection(), + NetworkDirection.PLAY_TO_CLIENT); + } catch (NullPointerException ignored) {} + } + + /** + * Gets a protected player from the list of protected players. + * @param playerUUID UUID of player to get. + * @return ProtectedPlayer instance if player is present, otherwise null. + */ + public static ProtectedPlayer getPlayer(UUID playerUUID) { + for (ProtectedPlayer protectedPlayer : protectedPlayers) { + if (protectedPlayer.getPlayerUUID().equals(playerUUID)) { + return protectedPlayer; + } + } + return null; + } + + /** + * Removes a player from the list of protected players. + * @param playerUUID UUID of player to remove. + */ + public static void removePlayer(UUID playerUUID) { + ProtectedPlayer player = getPlayer(playerUUID); + if (player == null) { + return; + } + + player.getTimer().cancel(); + protectedPlayers.remove(player); + } + + /** + * Updates a player's state appropriately. + * @param playerUUID UUID of player to update the state of. + */ + public static void updateState(UUID playerUUID) { + ProtectedPlayer protectedPlayer = getPlayer(playerUUID); + ServerPlayer player = LoginProtection.getProxy().getServer().getPlayerList().getPlayer(playerUUID); + if (player == null) { + removePlayer(playerUUID); + return; + } + + if (protectedPlayer == null) { + addPlayer(playerUUID); + } else { + ProtectedPlayer.State currentState = protectedPlayer.getState(); + if (currentState.equals(ProtectedPlayer.State.JOINING)) { + if (Config.Server.LOGIN_GRACE_ENABLED.get()) { + // JOINING -> LOGIN_GRACE + protectedPlayer.setState(ProtectedPlayer.State.LOGIN_GRACE); + startGraceTimer(playerUUID, Config.Server.LOGIN_GRACE_DURATION.get() * 1000); + } else { + // JOINING -> ACTIVE + protectedPlayer.setState(ProtectedPlayer.State.ACTIVE); + if (Config.Server.LOGIN_APPLY_POST_EFFECTS.get()) { + applyPostEffects(playerUUID); + } + if (Config.Server.AFK_PROTECTION_ENABLED.get()) { + startAfkTimer(playerUUID, Config.Server.AFK_TIME_THRESHOLD.get() * 1000); + } else { + removePlayer(playerUUID); + } + } + } else if (currentState.equals(ProtectedPlayer.State.LOGIN_GRACE) || + currentState.equals(ProtectedPlayer.State.AFK_GRACE)) { + // LOGIN_GRACE, AFK_GRACE -> ACTIVE + protectedPlayer.setState(ProtectedPlayer.State.ACTIVE); + if ((currentState.equals(ProtectedPlayer.State.LOGIN_GRACE) && Config.Server.LOGIN_APPLY_POST_EFFECTS.get()) || + (currentState.equals(ProtectedPlayer.State.AFK_GRACE) && Config.Server.AFK_APPLY_POST_EFFECTS.get())) { + applyPostEffects(playerUUID); + } + if (Config.Server.AFK_PROTECTION_ENABLED.get()) { + startAfkTimer(playerUUID, Config.Server.AFK_TIME_THRESHOLD.get() * 1000); + } else { + removePlayer(playerUUID); + } + } else if (currentState.equals(ProtectedPlayer.State.ACTIVE)) { + // ACTIVE -> AFK + protectedPlayer.setState(ProtectedPlayer.State.AFK); + } else if (currentState.equals(ProtectedPlayer.State.AFK)) { + if (Config.Server.AFK_GRACE_ENABLED.get()) { + // AFK -> AFK_GRACE + protectedPlayer.setState(ProtectedPlayer.State.AFK_GRACE); + startGraceTimer(playerUUID, Config.Server.AFK_GRACE_DURATION.get() * 1000); + } else { + // AFK -> ACTIVE + protectedPlayer.setState(ProtectedPlayer.State.ACTIVE); + if (Config.Server.AFK_APPLY_POST_EFFECTS.get()) { + applyPostEffects(playerUUID); + } + if (Config.Server.AFK_PROTECTION_ENABLED.get()) { + startAfkTimer(playerUUID, Config.Server.AFK_TIME_THRESHOLD.get() * 1000); + } else { + removePlayer(playerUUID); + } + } + } + + // Send state packet to player + NetworkManager.getChannel().sendTo(new PlayerStatePacket(protectedPlayer.getState()), + player.connection.getConnection(), + NetworkDirection.PLAY_TO_CLIENT); + } + } + + /** + * Starts the afk timer for a given player if they are a protected player. + * @param playerUUID The UUID of the player. + * @param delay After how much time should the task run? (in milliseconds) + */ + public static void startAfkTimer(UUID playerUUID, long delay) { + ProtectedPlayer player = getPlayer(playerUUID); + if (player == null) { + return; + } + + // Create scheduled task + player.setAfkTimerTask(new TimerTask() { + @Override + public void run() { + try { + // Send request for list input tick packet to player + NetworkManager.getChannel().sendTo(new RequestLastInputTickPacket(), + LoginProtection.getProxy().getServer().getPlayerList().getPlayer(playerUUID).connection.getConnection(), + NetworkDirection.PLAY_TO_CLIENT); + } catch (NullPointerException e) { + removePlayer(playerUUID); + } + } + }, delay); + } + + /** + * Starts the grace period timer for a given player if they are a protected player. + * @param playerUUID UUID of the player. + * @param delay How long should the grace period last? (in milliseconds) + */ + private static void startGraceTimer(UUID playerUUID, long delay) { + ProtectedPlayer player = getPlayer(playerUUID); + if (player == null) { + return; + } + + // Create scheduled task + player.setGracePeriodTimerTask(new TimerTask() { + @Override + public void run() { + // Update player state + updateState(playerUUID); + } + }, delay); + } + + /** + * Applies effects to the player. + * @param playerUUID UUID of player to apply effects to. + */ + private static void applyPostEffects(UUID playerUUID) { + // Get player entity + Player player = LoginProtection.getProxy().getServer().getPlayerList().getPlayer(playerUUID); + if (player == null) { + removePlayer(playerUUID); + return; + } + + // Apply effects + if (player.isInWater()) { + if (Config.Server.POST_REFILL_AIR_ENABLED.get()) { + player.setAirSupply(player.getMaxAirSupply()); + } + if (Config.Server.POST_WATER_ENABLED.get()) { + player.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, + Config.Server.POST_WATER_DURATION.get() * 20, 0)); + } + } + if (player.isInLava()) { + if (Config.Server.POST_LAVA_ENABLED.get()) { + player.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, + Config.Server.POST_LAVA_DURATION.get()*20, 0)); + } + } + if (player.isOnFire()) { + if (Config.Server.POST_FIRE_ENABLED.get()) { + player.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, + Config.Server.POST_FIRE_DURATION.get()*20, 0)); + } + } + } +} diff --git a/src/main/java/dev/micle/loginprotection/events/client/OnClientInputEventHandler.java b/src/main/java/dev/micle/loginprotection/events/client/OnClientInputEventHandler.java new file mode 100644 index 0000000..bd55fb8 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/events/client/OnClientInputEventHandler.java @@ -0,0 +1,345 @@ +package dev.micle.loginprotection.events.client; + +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.network.NetworkManager; +import dev.micle.loginprotection.network.client.InputPacket; +import dev.micle.loginprotection.proxy.Proxy; +import dev.micle.loginprotection.setup.Config; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.client.gui.screens.PauseScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import org.lwjgl.glfw.GLFW; + +import java.util.Arrays; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +public class OnClientInputEventHandler { + private static boolean isPausePressed = false; + private static boolean isDebugPressed = false; + private static boolean isFullscreenPressed = false; + private static boolean isTogglePerspectivePressed = false; + private static boolean isSmoothCameraPressed = false; + private static boolean isScreenshotPressed = false; + private static boolean isSpectatorOutlinesPressed = false; + private static boolean isAdvancementsPressed = false; + private static boolean isPlayerListPressed = false; + private static boolean isChatPressed = false; + private static boolean isChatCommandPressed = false; + private static boolean isChatEnterPressed = false; + private static boolean isSocialInteractionsPressed = false; + private static boolean isLoadHotbarActivatorPressed = false; + private static boolean isSaveHotbarActivatorPressed = false; + private static boolean isSwapOffhandPressed = false; + private static boolean isInventoryPressed = false; + private static boolean isDropItemPressed = false; + private static boolean isUseItemPressed = false; + private static boolean isPickBlockPressed = false; + private static boolean isAttackPressed = false; + private static boolean isMoveUpPressed = false; + private static boolean isMoveRightPressed = false; + private static boolean isMoveDownPressed = false; + private static boolean isMoveLeftPressed = false; + private static boolean isMoveSprintPressed = false; + private static boolean isSneakPressed = false; + private static boolean isJumpPressed = false; + private static final Boolean[] isHotBarPressed = new Boolean[9]; + + private static Screen previousScreen = null; + + public OnClientInputEventHandler() { + // Initialize isHotBarPressed to false + Arrays.fill(isHotBarPressed, false); + } + + @SubscribeEvent + public void KeyInputEvent(InputEvent.KeyInputEvent event) { + handle(event.getAction(), event.getKey()); + } + + @SubscribeEvent + public void MouseInputEvent(InputEvent.MouseInputEvent event) { + handle(event.getAction(), event.getButton()); + } + + private static void handle(int action, int key) { + // Initialize variables + Minecraft minecraft = Minecraft.getInstance(); + + // Cancel event if not in a game + if ((minecraft.getCurrentServer() == null && minecraft.getSingleplayerServer() == null) || + Proxy.Client.getPlayerState() == null) { + return; + } + + // Check if input is a press or release + if (action == GLFW.GLFW_PRESS || action == GLFW.GLFW_RELEASE) { + // Initialize variables + Options keyBinds = minecraft.options; + boolean isPressed = action == GLFW.GLFW_PRESS; + List allowedKeys = (Proxy.Client.getPlayerState().equals(ProtectedPlayer.State.JOINING)) ? + (List) Config.Server.LOGIN_KEY_ALLOW_LIST.get() : + (List) Config.Server.AFK_KEY_ALLOW_LIST.get(); + + // Check if the key is monitored, save it's state, update last input tick and notify server if not allowed + if (key == GLFW.GLFW_KEY_ESCAPE) { + isPausePressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.PAUSE.toString())) { + updateAndNotify(); + } + } else if (key == GLFW.GLFW_KEY_F3) { + isDebugPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.DEBUG.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyFullscreen.getKey().getValue()) { + isFullscreenPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.FULLSCREEN.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyTogglePerspective.getKey().getValue()) { + isTogglePerspectivePressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.PERSPECTIVE.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keySmoothCamera.getKey().getValue()) { + isSmoothCameraPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SMOOTH_CAMERA.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyScreenshot.getKey().getValue()) { + isScreenshotPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SCREENSHOT.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keySpectatorOutlines.getKey().getValue()) { + isSpectatorOutlinesPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SPECTATOR_OUTLINES.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyAdvancements.getKey().getValue()) { + isAdvancementsPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.ADVANCEMENTS.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyPlayerList.getKey().getValue()) { + isPlayerListPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.PLAYER_LIST.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyChat.getKey().getValue()) { + isChatPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.CHAT.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyCommand.getKey().getValue()) { + isChatCommandPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.CHAT.toString())) { + updateAndNotify(); + } + } else if (key == GLFW.GLFW_KEY_ENTER) { + isChatEnterPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.CHAT.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keySocialInteractions.getKey().getValue()) { + isSocialInteractionsPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SOCIAL_INTERACTIONS.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyLoadHotbarActivator.getKey().getValue()) { + isLoadHotbarActivatorPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.LOAD_HOTBAR_ACTIVATOR.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keySaveHotbarActivator.getKey().getValue()) { + isSaveHotbarActivatorPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SAVE_HOTBAR_ACTIVATOR.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keySwapOffhand.getKey().getValue()) { + isSwapOffhandPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SWAP_ITEM.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyInventory.getKey().getValue()) { + isInventoryPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.INVENTORY.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyDrop.getKey().getValue()) { + isDropItemPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.DROP_ITEM.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyUse.getKey().getValue()) { + isUseItemPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.USE_ITEM.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyPickItem.getKey().getValue()) { + isPickBlockPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.PICK_BLOCK.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyAttack.getKey().getValue()) { + isAttackPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.ATTACK.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyUp.getKey().getValue()) { + isMoveUpPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.MOVE.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyRight.getKey().getValue()) { + isMoveRightPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.MOVE.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyDown.getKey().getValue()) { + isMoveDownPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.MOVE.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyLeft.getKey().getValue()) { + isMoveLeftPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.MOVE.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keySprint.getKey().getValue()) { + isMoveSprintPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.MOVE.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyShift.getKey().getValue()) { + isSneakPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.SNEAK.toString())) { + updateAndNotify(); + } + } else if (key == keyBinds.keyJump.getKey().getValue()) { + isJumpPressed = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.JUMP.toString())) { + updateAndNotify(); + } + } else { + for (int i = 0; i < isHotBarPressed.length; i++) { + if (key == keyBinds.keyHotbarSlots[i].getKey().getValue()) { + isHotBarPressed[i] = isPressed; + if (!checkIfScreenIsAllowed(allowedKeys, minecraft.screen, !isPressed) && + !allowedKeys.contains(Config.Server.KEYS.HOTBAR.toString())) { + updateAndNotify(); + } + } + } + } + } + } + + private static boolean checkIfScreenIsAllowed(List allowedKeys, Screen screen, boolean isActionReleased) { + // Initialize variables + boolean isAllowed; + + // Check if the previous screen or the current screen are significant and their respective keys are allowed + if (previousScreen instanceof PauseScreen || screen instanceof PauseScreen) { + isAllowed = allowedKeys.contains(Config.Server.KEYS.PAUSE.toString()); + } else if (previousScreen instanceof InventoryScreen || screen instanceof InventoryScreen) { + isAllowed = allowedKeys.contains(Config.Server.KEYS.INVENTORY.toString()); + } else if (previousScreen instanceof ChatScreen || screen instanceof ChatScreen) { + isAllowed = allowedKeys.contains(Config.Server.KEYS.CHAT.toString()); + } else { + // Allow any other screen that is not the game itself + isAllowed = screen != null; + } + + // Only update the previous screen when key is released + if (isActionReleased) { + previousScreen = screen; + } + + // Return value + return isAllowed; + } + + private static void updateAndNotify() { + // Update last input tick + Proxy.Client.updateLastInputTick(); + + // Send an input packet if player is joining or is afk + if (Proxy.Client.getPlayerState().equals(ProtectedPlayer.State.JOINING) || + Proxy.Client.getPlayerState().equals(ProtectedPlayer.State.AFK)) { + NetworkManager.getChannel().sendToServer(new InputPacket()); + } + } + + public static void checkIfInputIsAllowed() { + // Get list of allowed keys + List allowedKeys = (Proxy.Client.getPlayerState().equals(ProtectedPlayer.State.JOINING)) ? + (List) Config.Server.LOGIN_KEY_ALLOW_LIST.get() : + (List) Config.Server.AFK_KEY_ALLOW_LIST.get(); + + // Update last input tick and notify server if any of the monitored keys are pressed and not allowed + if ((isPausePressed && !allowedKeys.contains(Config.Server.KEYS.PAUSE.toString())) || + (isDebugPressed && !allowedKeys.contains(Config.Server.KEYS.DEBUG.toString())) || + (isFullscreenPressed && !allowedKeys.contains(Config.Server.KEYS.FULLSCREEN.toString())) || + (isTogglePerspectivePressed && !allowedKeys.contains(Config.Server.KEYS.PERSPECTIVE.toString())) || + (isSmoothCameraPressed && !allowedKeys.contains(Config.Server.KEYS.SMOOTH_CAMERA.toString())) || + (isScreenshotPressed && !allowedKeys.contains(Config.Server.KEYS.SCREENSHOT.toString())) || + (isSpectatorOutlinesPressed && !allowedKeys.contains(Config.Server.KEYS.SPECTATOR_OUTLINES.toString())) || + (isAdvancementsPressed && !allowedKeys.contains(Config.Server.KEYS.ADVANCEMENTS.toString())) || + (isPlayerListPressed && !allowedKeys.contains(Config.Server.KEYS.PLAYER_LIST.toString())) || + ((isChatPressed || isChatCommandPressed || isChatEnterPressed) && + !allowedKeys.contains(Config.Server.KEYS.CHAT.toString())) || + (isSocialInteractionsPressed && !allowedKeys.contains(Config.Server.KEYS.SOCIAL_INTERACTIONS.toString())) || + (isLoadHotbarActivatorPressed && !allowedKeys.contains(Config.Server.KEYS.LOAD_HOTBAR_ACTIVATOR.toString())) || + (isSaveHotbarActivatorPressed && !allowedKeys.contains(Config.Server.KEYS.SAVE_HOTBAR_ACTIVATOR.toString())) || + (isSwapOffhandPressed && !allowedKeys.contains(Config.Server.KEYS.SWAP_ITEM.toString())) || + (isInventoryPressed && !allowedKeys.contains(Config.Server.KEYS.INVENTORY.toString())) || + (isDropItemPressed && !allowedKeys.contains(Config.Server.KEYS.DROP_ITEM.toString())) || + (isUseItemPressed && !allowedKeys.contains(Config.Server.KEYS.USE_ITEM.toString())) || + (isPickBlockPressed && !allowedKeys.contains(Config.Server.KEYS.PICK_BLOCK.toString())) || + (isAttackPressed && !allowedKeys.contains(Config.Server.KEYS.ATTACK.toString())) || + ((isMoveUpPressed || isMoveRightPressed || isMoveDownPressed || isMoveLeftPressed || isMoveSprintPressed) && + !allowedKeys.contains(Config.Server.KEYS.MOVE.toString())) || + (isSneakPressed && !allowedKeys.contains(Config.Server.KEYS.SNEAK.toString())) || + (isJumpPressed && !allowedKeys.contains(Config.Server.KEYS.JUMP.toString())) || + (Arrays.stream(isHotBarPressed).anyMatch(b -> b) && !allowedKeys.contains(Config.Server.KEYS.HOTBAR.toString()))) { + updateAndNotify(); + } + } +} diff --git a/src/main/java/dev/micle/loginprotection/events/common/OnLivingSetAttackTargetEventHandler.java b/src/main/java/dev/micle/loginprotection/events/common/OnLivingSetAttackTargetEventHandler.java new file mode 100644 index 0000000..8665116 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/events/common/OnLivingSetAttackTargetEventHandler.java @@ -0,0 +1,32 @@ +package dev.micle.loginprotection.events.common; + +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import dev.micle.loginprotection.setup.Config; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class OnLivingSetAttackTargetEventHandler { + @SubscribeEvent + public void LivingSetAttackTargetEvent(LivingSetAttackTargetEvent event) { + if (!(event.getTarget() instanceof Player target)) { + return; + } + + ProtectedPlayer player = ProtectedPlayerManager.getPlayer(target.getUUID()); + if (player == null) { + return; + } + + // Check if mob should ignore player + if ((player.getState().equals(ProtectedPlayer.State.JOINING) && Config.Server.LOGIN_MOBS_IGNORE_PLAYER.get()) || + (player.getState().equals(ProtectedPlayer.State.AFK) && Config.Server.AFK_MOBS_IGNORE_PLAYER.get()) || + (player.getState().equals(ProtectedPlayer.State.LOGIN_GRACE) && Config.Server.LOGIN_GRACE_MOBS_IGNORE_PLAYER.get()) || + (player.getState().equals(ProtectedPlayer.State.AFK_GRACE) && Config.Server.AFK_GRACE_MOBS_IGNORE_PLAYER.get())) { + ((Mob) event.getEntityLiving()).setTarget(null); + } + } + +} diff --git a/src/main/java/dev/micle/loginprotection/events/common/OnPlayerDamageEventHandler.java b/src/main/java/dev/micle/loginprotection/events/common/OnPlayerDamageEventHandler.java new file mode 100644 index 0000000..909a06f --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/events/common/OnPlayerDamageEventHandler.java @@ -0,0 +1,23 @@ +package dev.micle.loginprotection.events.common; + +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.event.entity.living.LivingDamageEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class OnPlayerDamageEventHandler { + @SubscribeEvent + public void LivingDamageEvent(LivingDamageEvent event) { + if (!(event.getEntity() instanceof Player player)) { + return; + } + + ProtectedPlayer protectedPlayer = ProtectedPlayerManager.getPlayer(player.getUUID()); + if (protectedPlayer == null || protectedPlayer.getState().equals(ProtectedPlayer.State.ACTIVE)) { + return; + } + + event.setCanceled(true); + } +} diff --git a/src/main/java/dev/micle/loginprotection/events/common/OnPlayerJoinEventHandler.java b/src/main/java/dev/micle/loginprotection/events/common/OnPlayerJoinEventHandler.java new file mode 100644 index 0000000..80a27fa --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/events/common/OnPlayerJoinEventHandler.java @@ -0,0 +1,17 @@ +package dev.micle.loginprotection.events.common; + +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class OnPlayerJoinEventHandler { + @SubscribeEvent + public void EntityJoinWorldEvent(PlayerEvent.PlayerLoggedInEvent event) { + if (!(event.getEntity() instanceof Player player)) { + return; + } + + ProtectedPlayerManager.addPlayer(player.getUUID()); + } +} diff --git a/src/main/java/dev/micle/loginprotection/events/common/OnPlayerLeaveEventHandler.java b/src/main/java/dev/micle/loginprotection/events/common/OnPlayerLeaveEventHandler.java new file mode 100644 index 0000000..7626d41 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/events/common/OnPlayerLeaveEventHandler.java @@ -0,0 +1,17 @@ +package dev.micle.loginprotection.events.common; + +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class OnPlayerLeaveEventHandler { + @SubscribeEvent + public void PlayerLeaveEvent(PlayerEvent.PlayerLoggedOutEvent event) { + if (!(event.getEntity() instanceof Player player)) { + return; + } + + ProtectedPlayerManager.removePlayer(player.getUUID()); + } +} diff --git a/src/main/java/dev/micle/loginprotection/mixin/GuiRenderTickMixin.java b/src/main/java/dev/micle/loginprotection/mixin/GuiRenderTickMixin.java new file mode 100644 index 0000000..04901f0 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/mixin/GuiRenderTickMixin.java @@ -0,0 +1,50 @@ +package dev.micle.loginprotection.mixin; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.proxy.Proxy; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.client.gui.ForgeIngameGui; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ForgeIngameGui.class) +@OnlyIn(Dist.CLIENT) +public class GuiRenderTickMixin { + @Inject(method = "render", at = @At(value = "HEAD")) + private void onClientTick(PoseStack poseStack, float partialTicks, CallbackInfo callbackInfo) { + // Setup + poseStack.pushPose(); + poseStack.translate(Minecraft.getInstance().getWindow().getGuiScaledWidth() / 2.0, + Minecraft.getInstance().getWindow().getGuiScaledHeight() / 2.0, 0); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + + // Render player state text + poseStack.pushPose(); + poseStack.scale(5, 5, 5); + + // Check if we should draw the state + if (Proxy.Client.getPlayerState() != null && !Proxy.Client.getPlayerState().equals(ProtectedPlayer.State.ACTIVE)) { + // Initialize variables + Font font = Minecraft.getInstance().font; + float offsetX = -(font.width(Proxy.Client.getPlayerState().toString()) / 2f); + float offsetY = -((font.lineHeight / 2f) + ((Minecraft.getInstance().getWindow().getGuiScaledHeight() / 4f) / 5)); + int argb = 0xFFFFFFFF; + + // Draw the player's protection state + font.drawShadow(poseStack, Proxy.Client.getPlayerState().toString().replace("_", " "), offsetX, offsetY, argb); + } + poseStack.popPose(); + + // Finish + RenderSystem.disableBlend(); + poseStack.popPose(); + } +} diff --git a/src/main/java/dev/micle/loginprotection/network/NetworkManager.java b/src/main/java/dev/micle/loginprotection/network/NetworkManager.java new file mode 100644 index 0000000..cca8353 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/network/NetworkManager.java @@ -0,0 +1,49 @@ +package dev.micle.loginprotection.network; + +import dev.micle.loginprotection.LoginProtection; +import dev.micle.loginprotection.network.client.InputPacket; +import dev.micle.loginprotection.network.client.LastInputTickPacket; +import dev.micle.loginprotection.network.server.PlayerStatePacket; +import dev.micle.loginprotection.network.server.RequestLastInputTickPacket; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.simple.SimpleChannel; + +public class NetworkManager { + private static SimpleChannel channel; + + public static void init() { + // Create channel + channel = NetworkRegistry.ChannelBuilder.named(LoginProtection.createResourceLocation("network")) + .clientAcceptedVersions(v -> v.equals(LoginProtection.getVersion())) + .serverAcceptedVersions(v -> v.equals(LoginProtection.getVersion())) + .networkProtocolVersion(LoginProtection::getVersion) + .simpleChannel(); + + // Register packets + int id = 0; + channel.messageBuilder(InputPacket.class, id++) + .encoder(InputPacket::encode) + .decoder(InputPacket::decode) + .consumer(InputPacket::handle) + .add(); + channel.messageBuilder(PlayerStatePacket.class, id++) + .encoder(PlayerStatePacket::encode) + .decoder(PlayerStatePacket::decode) + .consumer(PlayerStatePacket::handle) + .add(); + channel.messageBuilder(RequestLastInputTickPacket.class, id++) + .encoder(RequestLastInputTickPacket::encode) + .decoder(RequestLastInputTickPacket::decode) + .consumer(RequestLastInputTickPacket::handle) + .add(); + channel.messageBuilder(LastInputTickPacket.class, id++) + .encoder(LastInputTickPacket::encode) + .decoder(LastInputTickPacket::decode) + .consumer(LastInputTickPacket::handle) + .add(); + } + + public static SimpleChannel getChannel() { + return channel; + } +} diff --git a/src/main/java/dev/micle/loginprotection/network/client/InputPacket.java b/src/main/java/dev/micle/loginprotection/network/client/InputPacket.java new file mode 100644 index 0000000..b2f621c --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/network/client/InputPacket.java @@ -0,0 +1,41 @@ +package dev.micle.loginprotection.network.client; + +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class InputPacket { + public static void encode(final InputPacket packet, final FriendlyByteBuf buffer) {} + + public static InputPacket decode(final FriendlyByteBuf buffer) { + return new InputPacket(); + } + + public static void handle(final InputPacket packet, final Supplier contextSupplier) { + final NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + // Get sender + ServerPlayer sender = context.getSender(); + if (sender == null) { + return; + } + + // Get protected player + ProtectedPlayer protectedPlayer = ProtectedPlayerManager.getPlayer(sender.getUUID()); + if (protectedPlayer == null) { + return; + } + + // Update player state if they are joining or afk + if (protectedPlayer.getState().equals(ProtectedPlayer.State.JOINING) || + protectedPlayer.getState().equals(ProtectedPlayer.State.AFK)) { + ProtectedPlayerManager.updateState(protectedPlayer.getPlayerUUID()); + } + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/dev/micle/loginprotection/network/client/LastInputTickPacket.java b/src/main/java/dev/micle/loginprotection/network/client/LastInputTickPacket.java new file mode 100644 index 0000000..3126762 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/network/client/LastInputTickPacket.java @@ -0,0 +1,56 @@ +package dev.micle.loginprotection.network.client; + +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import dev.micle.loginprotection.proxy.Proxy; +import dev.micle.loginprotection.setup.Config; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class LastInputTickPacket { + private final int lastInputTick; + + public LastInputTickPacket() { + this(Proxy.Client.getLastInputTick()); + } + public LastInputTickPacket(int lastInputTick) { + this.lastInputTick = lastInputTick; + } + + public static void encode(final LastInputTickPacket packet, final FriendlyByteBuf buffer) { + buffer.writeInt(packet.lastInputTick); + } + + public static LastInputTickPacket decode(final FriendlyByteBuf buffer) { + return new LastInputTickPacket(buffer.readInt()); + } + + public static void handle(final LastInputTickPacket packet, final Supplier contextSupplier) { + final NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + // Get sender + ServerPlayer sender = context.getSender(); + if (sender == null) { + return; + } + + // Get protected player + ProtectedPlayer protectedPlayer = ProtectedPlayerManager.getPlayer(sender.getUUID()); + if (protectedPlayer == null) { + return; + } + + // Check if player is afk + if (sender.tickCount - packet.lastInputTick >= Config.Server.AFK_TIME_THRESHOLD.get() * 20) { + ProtectedPlayerManager.updateState(sender.getUUID()); // Update state + } else { + ProtectedPlayerManager.startAfkTimer(sender.getUUID(), (long) ((Config.Server.AFK_TIME_THRESHOLD.get() - + ((sender.tickCount - packet.lastInputTick) / 20.0)) * 1000)); // Start new afk timer + } + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/dev/micle/loginprotection/network/server/PlayerStatePacket.java b/src/main/java/dev/micle/loginprotection/network/server/PlayerStatePacket.java new file mode 100644 index 0000000..334c00d --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/network/server/PlayerStatePacket.java @@ -0,0 +1,32 @@ +package dev.micle.loginprotection.network.server; + +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.proxy.Proxy; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class PlayerStatePacket { + private final ProtectedPlayer.State state; + + public PlayerStatePacket(ProtectedPlayer.State state) { + this.state = state; + } + + public static void encode(final PlayerStatePacket packet, final FriendlyByteBuf buffer) { + buffer.writeUtf(packet.state.toString()); + } + + public static PlayerStatePacket decode(final FriendlyByteBuf buffer) { + return new PlayerStatePacket(ProtectedPlayer.State.valueOf(buffer.readUtf())); + } + + public static void handle(final PlayerStatePacket packet, final Supplier contextSupplier) { + final NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + Proxy.Client.setPlayerState(packet.state); + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/dev/micle/loginprotection/network/server/RequestLastInputTickPacket.java b/src/main/java/dev/micle/loginprotection/network/server/RequestLastInputTickPacket.java new file mode 100644 index 0000000..f7974d2 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/network/server/RequestLastInputTickPacket.java @@ -0,0 +1,30 @@ +package dev.micle.loginprotection.network.server; + +import dev.micle.loginprotection.events.client.OnClientInputEventHandler; +import dev.micle.loginprotection.network.NetworkManager; +import dev.micle.loginprotection.network.client.LastInputTickPacket; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class RequestLastInputTickPacket { + public static void encode(final RequestLastInputTickPacket packet, final FriendlyByteBuf buffer) { + } + + public static RequestLastInputTickPacket decode(final FriendlyByteBuf buffer) { + return new RequestLastInputTickPacket(); + } + + public static void handle(final RequestLastInputTickPacket packet, final Supplier contextSupplier) { + final NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + // Update last input tick (This works around GLFW forgetting about a repeat key after another one is pressed) + OnClientInputEventHandler.checkIfInputIsAllowed(); + + // Send last input tick packet back to server + NetworkManager.getChannel().sendToServer(new LastInputTickPacket()); + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/dev/micle/loginprotection/proxy/IProxy.java b/src/main/java/dev/micle/loginprotection/proxy/IProxy.java new file mode 100644 index 0000000..3842dd0 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/proxy/IProxy.java @@ -0,0 +1,11 @@ +package dev.micle.loginprotection.proxy; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; + +public interface IProxy { + MinecraftServer getServer(); + Player getClientPlayer(); + Level getClientWorld(); +} diff --git a/src/main/java/dev/micle/loginprotection/proxy/Proxy.java b/src/main/java/dev/micle/loginprotection/proxy/Proxy.java new file mode 100644 index 0000000..d1d0440 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/proxy/Proxy.java @@ -0,0 +1,145 @@ +package dev.micle.loginprotection.proxy; + +import dev.micle.loginprotection.LoginProtection; +import dev.micle.loginprotection.data.ProtectedPlayer; +import dev.micle.loginprotection.data.ProtectedPlayerManager; +import dev.micle.loginprotection.events.client.OnClientInputEventHandler; +import dev.micle.loginprotection.events.common.OnLivingSetAttackTargetEventHandler; +import dev.micle.loginprotection.events.common.OnPlayerDamageEventHandler; +import dev.micle.loginprotection.events.common.OnPlayerJoinEventHandler; +import dev.micle.loginprotection.events.common.OnPlayerLeaveEventHandler; +import dev.micle.loginprotection.network.NetworkManager; +import dev.micle.loginprotection.setup.Config; +import net.minecraft.client.Minecraft; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.AddReloadListenerEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.event.lifecycle.*; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +public class Proxy implements IProxy { + // Initialize variables + private static MinecraftServer server = null; + + // Common setup + public Proxy() { + // Initialize setup + Config.init(); + NetworkManager.init(); + + // Register mod event bus listeners + IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); + modEventBus.addListener(Proxy::setup); + modEventBus.addListener(Proxy::imcEnqueue); + modEventBus.addListener(Proxy::imcProcess); + + // Register event but listeners + MinecraftForge.EVENT_BUS.addListener(Proxy::onAddReloadListeners); + MinecraftForge.EVENT_BUS.addListener(Proxy::serverStarted); + MinecraftForge.EVENT_BUS.addListener(Proxy::serverStopping); + + // Register event handlers + MinecraftForge.EVENT_BUS.register(new OnLivingSetAttackTargetEventHandler()); + MinecraftForge.EVENT_BUS.register(new OnPlayerDamageEventHandler()); + MinecraftForge.EVENT_BUS.register(new OnPlayerJoinEventHandler()); + MinecraftForge.EVENT_BUS.register(new OnPlayerLeaveEventHandler()); + } + + private static void setup(FMLCommonSetupEvent event) {} + + private static void imcEnqueue(InterModEnqueueEvent event) {} + + private static void imcProcess(InterModProcessEvent event) {} + + private static void onAddReloadListeners(AddReloadListenerEvent event) {} + + private static void serverStarted(ServerStartedEvent event) { + ProtectedPlayerManager.init(); + server = event.getServer(); + } + + private static void serverStopping(ServerStoppingEvent event) { + server = null; + } + + @Override + public MinecraftServer getServer() { + return server; + } + + @Override + public Player getClientPlayer() { + return null; + } + + @Override + public Level getClientWorld() { + return null; + } + + // Client setup + public static class Client extends Proxy { + private static ProtectedPlayer.State playerState; + private static int lastInputTick; + + public Client() { + // Register mod event bus listeners + IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); + modEventBus.addListener(Client::setup); + modEventBus.addListener(Client::postSetup); + + // Register event handlers + MinecraftForge.EVENT_BUS.register(new OnClientInputEventHandler()); + } + + private static void setup(FMLClientSetupEvent event) {} + + private static void postSetup(FMLLoadCompleteEvent event) {} + + public static ProtectedPlayer.State getPlayerState() { + return playerState; + } + + public static void setPlayerState(ProtectedPlayer.State newPlayerState) { + playerState = newPlayerState; + } + + public static int getLastInputTick() { + return lastInputTick; + } + + public static void updateLastInputTick() { + lastInputTick = LoginProtection.getProxy().getClientPlayer().tickCount; + } + + @Override + @OnlyIn(Dist.CLIENT) + public Player getClientPlayer() { + return Minecraft.getInstance().player; + } + + @Override + @OnlyIn(Dist.CLIENT) + public Level getClientWorld() { + return Minecraft.getInstance().level; + } + } + + // Server setup + public static class Server extends Proxy { + public Server() { + // Register mod event bus listeners + IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); + modEventBus.addListener(Server::setup); + } + + private static void setup(FMLDedicatedServerSetupEvent event) {} + } +} diff --git a/src/main/java/dev/micle/loginprotection/setup/Config.java b/src/main/java/dev/micle/loginprotection/setup/Config.java new file mode 100644 index 0000000..933bb57 --- /dev/null +++ b/src/main/java/dev/micle/loginprotection/setup/Config.java @@ -0,0 +1,174 @@ +package dev.micle.loginprotection.setup; + +import com.google.common.collect.Lists; +import dev.micle.loginprotection.LoginProtection; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; + +@Mod.EventBusSubscriber(modid = LoginProtection.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD) +public final class Config { + public static final Server SERVER; + public static final ForgeConfigSpec SERVER_SPEC; + static { + Pair spec_pair = new ForgeConfigSpec.Builder().configure(Server::new); + SERVER = spec_pair.getLeft(); + SERVER_SPEC = spec_pair.getRight(); + } + + public static void init() { + ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, SERVER_SPEC); + } + + public static class Server { + public static ForgeConfigSpec.BooleanValue POST_REFILL_AIR_ENABLED; + public static ForgeConfigSpec.BooleanValue POST_WATER_ENABLED; + public static ForgeConfigSpec.IntValue POST_WATER_DURATION; + public static ForgeConfigSpec.BooleanValue POST_LAVA_ENABLED; + public static ForgeConfigSpec.IntValue POST_LAVA_DURATION; + public static ForgeConfigSpec.BooleanValue POST_FIRE_ENABLED; + public static ForgeConfigSpec.IntValue POST_FIRE_DURATION; + + public static ForgeConfigSpec.BooleanValue LOGIN_MOBS_IGNORE_PLAYER; + public static ForgeConfigSpec.BooleanValue LOGIN_APPLY_POST_EFFECTS; + public static ForgeConfigSpec.ConfigValue> LOGIN_KEY_ALLOW_LIST; + public static ForgeConfigSpec.BooleanValue LOGIN_GRACE_ENABLED; + public static ForgeConfigSpec.BooleanValue LOGIN_GRACE_MOBS_IGNORE_PLAYER; + public static ForgeConfigSpec.IntValue LOGIN_GRACE_DURATION; + + public static ForgeConfigSpec.BooleanValue AFK_PROTECTION_ENABLED; + public static ForgeConfigSpec.IntValue AFK_TIME_THRESHOLD; + public static ForgeConfigSpec.BooleanValue AFK_APPLY_POST_EFFECTS; + public static ForgeConfigSpec.BooleanValue AFK_MOBS_IGNORE_PLAYER; + public static ForgeConfigSpec.ConfigValue> AFK_KEY_ALLOW_LIST; + public static ForgeConfigSpec.BooleanValue AFK_GRACE_ENABLED; + public static ForgeConfigSpec.BooleanValue AFK_GRACE_MOBS_IGNORE_PLAYER; + public static ForgeConfigSpec.IntValue AFK_GRACE_DURATION; + + Server(ForgeConfigSpec.Builder builder) { + builder.comment("Settings for protecting players while they are joining.").push("Login"); + LOGIN_APPLY_POST_EFFECTS = builder + .comment("Whether to apply any post protection effects to joining players.") + .define("applyPostProtectionEffects", true); + LOGIN_MOBS_IGNORE_PLAYER = builder + .comment("Whether mobs will ignore a protected player. (They will not attack/aggro)") + .define("mobsIgnorePlayer", true); + builder.push("AllowedKeys"); + LOGIN_KEY_ALLOW_LIST = builder + .comment("Allowed keys players can press without becoming active.\n" + + "Available values: PAUSE, DEBUG, FULLSCREEN, PERSPECTIVE, SMOOTH_CAMERA, SCREENSHOT, SPECTATOR_OUTLINES,\n" + + "ADVANCEMENTS, PLAYER_LIST, CHAT, SOCIAL_INTERACTIONS, LOAD_HOTBAR_ACTIVATOR, SAVE_HOTBAR_ACTIVATOR,\n" + + "SWAP_ITEM, INVENTORY, HOTBAR, DROP_ITEM, USE_ITEM, PICK_BLOCK, ATTACK, MOVE, SNEAK, JUMP") + .defineList("allowedKeys", Lists.newArrayList(KEYS.PAUSE.toString(), KEYS.DEBUG.toString(), KEYS.FULLSCREEN.toString(), KEYS.PERSPECTIVE.toString(), KEYS.SMOOTH_CAMERA.toString(), + KEYS.SCREENSHOT.toString(), KEYS.SPECTATOR_OUTLINES.toString(), KEYS.ADVANCEMENTS.toString(), KEYS.PLAYER_LIST.toString(), KEYS.CHAT.toString(), KEYS.SOCIAL_INTERACTIONS.toString(), + KEYS.LOAD_HOTBAR_ACTIVATOR.toString(), KEYS.SAVE_HOTBAR_ACTIVATOR.toString(), KEYS.SWAP_ITEM.toString(), KEYS.HOTBAR.toString(), KEYS.PICK_BLOCK.toString()), o -> o instanceof String); + builder.pop(); + builder.push("Grace"); + LOGIN_GRACE_ENABLED = builder + .comment("Whether a player receives a grace period after becoming active or not.") + .define("graceEnabled", true); + LOGIN_GRACE_MOBS_IGNORE_PLAYER = builder + .comment("Whether mobs ignore the player during their grace period.") + .define("graceMobsIgnorePlayer", true); + LOGIN_GRACE_DURATION = builder + .comment("How long the grace period lasts in seconds.") + .defineInRange("graceDuration", 10, 1, Integer.MAX_VALUE); + builder.pop(); + builder.pop(); + + builder.comment("Settings for protecting players that are afk.").push("AFK"); + AFK_PROTECTION_ENABLED = builder + .comment("Enable protection of afk players?") + .define("enabled", true); + AFK_TIME_THRESHOLD = builder + .comment("How long a player needs to be afk to become protected. (seconds)") + .defineInRange("timeThreshold", 600, 1, Integer.MAX_VALUE/20); + AFK_APPLY_POST_EFFECTS = builder + .comment("Whether to apply any post protection effects to afk players.") + .define("applyPostProtectionEffects", false); + AFK_MOBS_IGNORE_PLAYER = builder + .comment("Whether mobs will ignore a protected player. (They will not attack/aggro") + .define("mobsIgnorePlayerEnabled", true); + builder.push("AllowedKeys"); + AFK_KEY_ALLOW_LIST = builder + .comment("Allowed keys players can press without becoming active.\n" + + "Available values: PAUSE, DEBUG, FULLSCREEN, PERSPECTIVE, SMOOTH_CAMERA, SCREENSHOT, SPECTATOR_OUTLINES,\n" + + "ADVANCEMENTS, PLAYER_LIST, CHAT, SOCIAL_INTERACTIONS, LOAD_HOTBAR_ACTIVATOR, SAVE_HOTBAR_ACTIVATOR,\n" + + "SWAP_ITEM, INVENTORY, HOTBAR, DROP_ITEM, USE_ITEM, PICK_BLOCK, ATTACK, MOVE, SNEAK, JUMP") + .defineList("allowedKeys", Lists.newArrayList(KEYS.PAUSE.toString(), KEYS.FULLSCREEN.toString(), KEYS.SCREENSHOT.toString(), KEYS.ADVANCEMENTS.toString()), o -> o instanceof String); + builder.pop(); + builder.push("Grace"); + AFK_GRACE_ENABLED = builder + .comment("Whether a player receives a grace period after becoming active or not.") + .define("graceEnabled", false); + AFK_GRACE_MOBS_IGNORE_PLAYER = builder + .comment("Whether mobs ignore the player during their grace period.") + .define("graceMobsIgnorePlayer", true); + AFK_GRACE_DURATION = builder + .comment("How long the grace period lasts in seconds.") + .defineInRange("graceDuration", 5, 1, Integer.MAX_VALUE); + builder.pop(); + builder.pop(); + + builder.comment("Additional protection settings that apply as soon as a player becomes active if enabled.").push("Post"); + builder.push("WaterProtection"); + POST_REFILL_AIR_ENABLED = builder + .comment("Whether a player's air supply gets refilled.") + .define("refillAir", true); + POST_WATER_ENABLED = builder + .comment("Whether a player receives water breathing when in water.") + .define("waterEnabled", false); + POST_WATER_DURATION = builder + .comment("Water breathing duration in seconds.") + .defineInRange("waterDuration", 10, 1, Integer.MAX_VALUE/20); + builder.pop(); + builder.push("LavaProtection"); + POST_LAVA_ENABLED = builder + .comment("Whether a player receives fire resistance when in lava.") + .define("enabled", true); + POST_LAVA_DURATION = builder + .comment("Fire resistance duration in seconds.") + .defineInRange("duration", 10, 1, Integer.MAX_VALUE/20); + builder.pop(); + builder.push("FireProtection"); + POST_FIRE_ENABLED = builder + .comment("Whether a player receives fire resistance when on fire.") + .define("enabled", false); + POST_FIRE_DURATION = builder + .comment("Fire resistance duration in seconds.") + .defineInRange("duration", 10, 1, Integer.MAX_VALUE/20); + builder.pop(); + builder.pop(); + } + + public enum KEYS { + PAUSE, + DEBUG, + FULLSCREEN, + PERSPECTIVE, + SMOOTH_CAMERA, + SCREENSHOT, + SPECTATOR_OUTLINES, + ADVANCEMENTS, + PLAYER_LIST, + CHAT, + SOCIAL_INTERACTIONS, + LOAD_HOTBAR_ACTIVATOR, + SAVE_HOTBAR_ACTIVATOR, + SWAP_ITEM, + INVENTORY, + HOTBAR, + DROP_ITEM, + USE_ITEM, + PICK_BLOCK, + ATTACK, + MOVE, + SNEAK, + JUMP + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..72cfd47 --- /dev/null +++ b/src/main/resources/META-INF/mods.toml @@ -0,0 +1,58 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the forge version +loaderVersion = "[40,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "All Rights Reserved" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="http://my.issue.tracker/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "loginprotection" #mandatory +# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it +# ${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata +# see the associated build.gradle script for how to populate this completely automatically during a build +version = "${file.jarVersion}" #mandatory +# A display name for the mod +displayName = "Micle's Login Protection" #mandatory +# A URL to query for updates for this mod. See the JSON update specification +#updateJSONURL="http://myurl.me/" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="http://example.com/" #optional +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="logo.jpg" #optional +# A text field displayed in the mod UI +#credits="Thanks for this example mod goes to Java" #optional +# A text field displayed in the mod UI +authors = "Micle" #optional +# The description text for the mod (multi line!) (#mandatory) +description = ''' +Protects players from damage while they are are joining a server or are AFK. +''' +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies.loginprotection]] #optional + # the modid of the dependency + modId = "forge" #mandatory + # Does this dependency have to exist - if not, ordering below must be specified + mandatory = true #mandatory + # The version range of the dependency + versionRange = "[40,)" #mandatory + # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory + ordering = "NONE" + # Side this dependency is applied on - BOTH, CLIENT or SERVER + side = "BOTH" +# Here's another dependency +[[dependencies.loginprotection]] + modId = "minecraft" + mandatory = true + # This version range declares a minimum of the current minecraft version up to but not including the next major version + versionRange = "[1.18.2,1.19)" + ordering = "NONE" + side = "BOTH" diff --git a/src/main/resources/loginprotection.mixins.json b/src/main/resources/loginprotection.mixins.json new file mode 100644 index 0000000..df87769 --- /dev/null +++ b/src/main/resources/loginprotection.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.micle.loginprotection.mixin", + "compatibilityLevel": "JAVA_17", + "refmap": "loginprotection.refmap.json", + "mixins": [ + ], + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "GuiRenderTickMixin" + ] +} \ No newline at end of file diff --git a/src/main/resources/logo.jpg b/src/main/resources/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..48c2bba68cd309c132b0346689ba60b726794ee4 GIT binary patch literal 31912 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

EQ)w|d8bNrn`c0omUa3I2b zLvAc61}^rT5oA;X%kPi^<+NpiRoix-$yPdStz_AP<$>)&Iw5gMOQmylYjXWzNGD`~ zc-d*q`kGDYQS$t{%D%Lc?Cb)`CG-Ncfs&29^Uo(@bz!| z@w@WDkS`qO>ml7riUru3&fi+5gLh8>jZiQtqnyBd5W=KDeZuY^az5vBZ&0JpF=w?w zOcA!W^SAu4SLn+8zI64PKfHJIVAguCHyr-K=fhzZ?gRD*A>i(`)Q-e49X_Y* z7foQ4N?R2=Cq+HuZGShdMTD}KZHpiYGXLmr@fOi5I=JPJ5zh{ighH-@Kg$?q_7#GO1XfR88t*4Yrrz^9r})5h+{N*_ky~$$nCDS$%F-0%wi$ zzwdKc-VRneSjMUtQ-QzMuu{qBVO?jjq2!gh?INzG#_Gis zTkTOxiLvSInmsd4ZS!O|Mx6*Qijkn-0MMCS*j9#X3s2jU7w-7EIC&HPwq&l=(e&Wa zJNMwE-v`nyZl?ee6g57Rwlq2S@iT|t_eT=0`p()qMJlKLZdz)4ez1g6&ivhU3Z%8} z&76Gi(ocRIoTIlJ_;KmPEcn3oOGqGWPE z68Itgf9vz;Q3A?!pyaSLzaM~g0n7mCRR_Q=1ON~OC?!@~^2L(3CJ#Nc776D|4`W*l z2b@+~JhKYe-{d6ZcW&KIg=f(qNCBz?7F({6;9m|iPY46N)HYx0O^>gy`^$AMoN&YO z!b^PlBRW)_;p)z_*%6VkF5p(Hq}UGx7Y!R5{cS~Yp-X0&=Ss@-iDRPn%OI?p=^e%m z)Lo4*?i&726Lkyu9NL}ylQk6|Yll7re+(Q&Zn?$Cv#O_9Ox?>evg!0ujtkdcH|jSU z<^9lhJkS@>wEr&3>!07rtXShYw#(e0{XTcmIu(Sec&xJL?1Uv!H|-;p@w!`L@_X<) z{3WM0M&gUVslWB>q#WxfvNodSO_ol*t4qzXwF7tVtqP+Wf<9vw%Q4-IjaK++RP*AF zOQcGRsFb5ZG6hpnr19z(j!^St$q_yO;G1sasg6}9J^^Jb!8Q)K9VBkYH@@tWc_mtT zzb$yF7d%n9c@r}m(o1Q-KESqm@u&nxWPeb*aEEq&4%v=4er_o@P8_NzS3hAFqKKaik0AN2BY+x&}RJ%PR)_2H++L1rT=FA`Z zWG#OCH!cH!c~{Q?$qzy?c24CD$lK~)>ZibkR)M4#Jm&@M0h3RPiGtxp`fT|N^q>bC z!T%h3?xinO)O3oLt0LOyeF@bKlrsBc2t6d$Ek?;4sy%{smARqE6A&w%NHOkG?Fi;yXAsn;&5o!-aK1>5p67|3Kqe!^UFeOI-;-q8%br>@Pg7g;U@*vUgvjd-G zyZ{$**cl64{tp&_v+#@s=_gr>2d!$WAw}S<>;6Aj5I*~^@DzZD7;^T!t8yvYI|Z8N zTRG>KPBf;w;*&d~L3eaYk}4N%B7jn`iFm+sIm|TN!7Qxqh5Hx;LeE5RPTs}C(KB2x zQyj>~Q)gPwV0FV9m~__wcc30*KW2)BiEyOkC&Bb`R+*}N&l~?rmz}J~<^nYGy6Hq0 z05Um%L=y1oqO6EZgs6i%;{Z?$0A5D)7c*ZjJQ2%Z5k0&kbTTm+spdDd8A^SJU}(5x zk=EHd#sz%kLU5C|e0lkH<-X^U0N%ANcW>L8^8;JsfZu(9s!K)&0rvJs`~b@5NdYYo zSxw@^2vX{>E_b2_0R4kk06+>#C?MB}9t$5c%XI47@fX}eyl6n2f&bsc;tMxT>pob7 z7s>r#0Nuj-2P~B=VE(jDMX74P=v-D28qs*CvoGcGqAj1`+M+E!ZO$LdKE$Wh( z>!ONiqo6#(2ARl-(ub(WNDV%V%sc~=T&8NMj2EljuvEKu2CIg4tm_~r71{9=Yw_-T zx-v-urQ}cz=tg;D2LLPu2mrXnaki)V9^9>-Aa*9PcYB(t(~Xk8Un_Y?N7p*N-(rv^ z7IAbaDr>s^PVBYqCl%C52|%F<7{32Jrhi2WytOlqggOFOBxJ!cvscm_M7U*Jk6OQ+$ z@j_pAV~%$Yl%kJq6F_4kv^UdTzx{;;KDI>k)5HrRZ4D2T+L*A$;%svkT0oHhlf`gh zbbl&10M$B#iyGg5aQ7e?eC7V(`IG93C)Jwwq`E6Kjc+;WnDO#I^Dw4=`6#BNE_O5S zilecyOSGd}3zF$$M5*#1$x15%JZnv!fY8ckC6>r1mY@NM9)LrK_-vw%nwzSc3TWp! zGq{%pSq^5ROn{S?xBU~yu%sw&fuWkiD z$t>eWw}VAhjCwf*UBMSAL~=6yVmdg&`x~zYY8+lW&arLZZ@qj9*nzr*I8k#{77LDb z+_Ic{^#X?5-^%ryr4vWD zJC{;}zN9=MBgt-vzkvR=C#?3DVqIc&G_;#_HM%}Wat}Wor8+P&2R(1foB6WCAz^jY z&iY)AteP+wD=olE9?Rq6{2n~)4Uk2RAb5Q%E+M!!tgduHNs1WnGPd(Z>qR41B*Z`U zPXBfz44(oJKaZt-zLQ^Ro60mQ18TpKR|jU<0YL^*EwQnM zIt;ODeyPw@GR=;tex( zE~?t@dwJzhqUE~0P0v{SM;-~uk|Hu3?dnd?(8=x@4x>WC$%W7A1}!?$3xyb1=NV&N zGpL3Y$?&8zP%=CXGMMqf@{G=2jGE>JU>n9wG6)_wV}%h#kv}2_VzVml4w4Dv)#*!; zy$%?H5z}ny6DOtjCz7XUqM+2hQew96Pl}#~Hmx+blC)0ZkH6=y-c^}ax@lO7Aes!b zy>xYvu6Xy}Dd64OZj;rsduJoRKeTDJ>BifNKm;H;2)Ka59F!qvB8BJ-e~>P70VG3! zI2g16kpqaxxWxOnHV6FfiWSWhaI8_C_;~nt&S&0)+)1TY^R#X(jy=@XXH~p$@@ja* z!GoYS_pm>`{MSWtRpo2}tc0JjKz8|^)|sgBiCkI#ThwIz4^i`EH*!1PRv?2Ttq%Uu z=Rm1Dc%yxP7n#FKm_SE!)QG+Nu3{_%+@)KHLFq<0@jO&}V8Z2HDA)meB6yFd zJ&vpAz^J_FO~k576e1v)={tB#UNeC8!V|>#Yw7Bw4x!IyW%s}#&U^w0GEUtA_Q*-UT z*pswRoH}b`C%?Mk(>vc~Yh{l(Tz+zlq`ZAIQVp;;yAvapd2|1YK~Sa#Il{~k*7^Pq zq$GH7CAk1F5Rts3lSc+|U(T~dbe!op6uZQh zpJqC=-FToT9Q2|XjNUk4F$#i%Sl1pXy?Y0De)pZP8=nCEGI#7rh&izytEp#W^H;s@ zs4K?WIKU~B#K#n)XUBtc)Nr=&8g#N(%0jc?7|G2T({8(5i+})a z5`)=>n>|xMDzf#Esz$r2;K?q#W@9Fvg|jmy*^x$0X(<$G;F0&h-#u*0Ex*Z0!7&Gr zQRG?ZkUIRdKp@-R-Il)cqR811&8s_jH^R|4_eGI*sxP+{D{{0=nKW6O*-jst#gte5 z#1TF71AC)enWL@qw`Wyc3 z(mK2@o8G4o-hspG{e-$w&{R)96q!_B3VJ2__n3J^85;$2#XcayM+?K7Yz70PQBTDU|A_fs$ z`6r6o2=e#fddp4;1d&UEj^ zaHj5nN@7LDY?Zw+w|OFC66<7@ALs4QIFd^j7b;vg~af_#ni$xCTk7 zx6+iX>v$PQYx3ZkS+5K7VHxXJsvgb84m4{|lV$rn3*>ihkf2q<{aBUcU(ONa zkAU}#?*|8+_vs^b3loPTogKBEw_z*P%dA9=`eb^t7c-w4E2!6A2~Z}lf66NEdciFG z63<-}O8QCC*erH&sr#3bA?==T`f<+J<@-9{vr>-2NQU3@Lh~nf?|`2Ffqh#YTj{l> zgU_vxx9j@}->y!CQOl4Ko#FjADImb8EO20hL=z2=aFHVcK#d|}U}rLDaHRNft;PLo z1mbKf>wge=&?#^>%sV(ih_kjs@>gpP;IeaP@Sg&g?(Uqak62DYbiy-fa5f`*4g~k% z&Q<$GV$)kT33V!$Q_}acq_+~)ovfaYzu`+<`Etw{kefOC@g`#Y2=LDS2`oW60ci7gM%>(n% zFN%A2i|hdLiI)N3qX2;Ge_#vhnLrpv7eGNt(!Z+mRxC^Ax#1V|mgBVK*=FMZBv8FC zZ!zrKZyi-S1wIjYK}{6o#MwmHI{tQb^-RN5!bh0)fNmgg=;Mulhrkcbv)28aEVhGl z30M2J5+uLOHQUa#E?W0O`xpFYe1wA6>4|Yjr#Nf!^h#dF>I@CLVKd_!I;^@S7JE>K zbeR;BniP}TaQ(u#l-%qLhvrgAS8`ua>L?7R*i(sI15X2TTcYWc-P0FFDeKg&%3b3m z;;PXx)+U;bG9Th;ynUW|MrY_&E_&GvxL00txHUfGOPMy!n7{lXawzzrxa%W8owS1H zTnXQD-8YBfha3^lc`N~r=R8JrbZYvaUXVH0jp|a;sa?N#A#vNS`{|%Edu-q6OJ$Dm z$8X})LH|; zUnIqLr`9P@5=skIc0>BUA31I6D zwz50RrsyZ$;eQPT7Ce5wJs#ik!zF(t8Gj;H2fDxKj;ua?F^I@+P1~gF-9H(j@f_H( z48mr4p8~~AC!VdipZD<}yMyNE-c-hKe+w0`^xX0Io>kQA+kXlu1=jYk_0Da^XNk%? z`h0I)vBB=;KihHP6frQ`R_i}W{XtmSPguD__Z=JUKe{KcHU!;jEocmx+|P$I|L9~x zXC35tDEAv3*mCae7b$+ecRx)QciBnc3c-oPu=9SF#+xtcducNMuX_6$+{^cxuH}eS z+q2H=fL67cu}Mvw%#E`XBDE>mNb;wZ4jm}nbeRlDcj`csxT2|x)eWQ4pfO$O6Y^(S zh*B{4!i*R^FIF#I%#0(w`M7}Tu}2Jb&{uoQqy`Z_be!4bGq-8;EAAfRHoZ-JuRP*a zpSAp`CixjA`)=nwpz~CmJ~=Fwh~(xqW%}0_!BD!(JgT|3i=Tcwe03(7sZ*#m`X8ye7@w-s;$P0jd^NyBIasPp_G&?oXY zfy>{S02o+pY30`sXszl__UZ!txBnosl`RgTIn%UHOV>MtCywyJVq$L-Kpi_IV*1^S zRgadF#S+#f|Id*l%rsm87dwY^f?p(lKo{JPk5~T2DZE_@mtFCb^Ts-U{grdcX2a_B zY_nExndj#wA$v4QqKN+MUnVYjP@(Je;5naeRyVu5d3Q@lqZ4Uq!lI~u46>h{2xc$1 zMFIvsx_XG4d;iMlX@rI4?b?{F4G@Smb0CSN&$vpbiJ@M-8(@~8T70%|!5TkQ@c7B4 z7x{y(eAl(|X)xCYha=)O!O%qkUh;@oDal4Kt|OYV8_>L#qHpEQCnH?0?qph+${5ap zeGJ;oN8jSbs1IX{%IXgj&h5JF-{08LVWB7pyOpc*`h)1PV#0-pOT`Ul#eVRRG)Y9rxghsWs+t0%4eB5hni?mud9#e0^5ZmF-ipy=I!|B=La zP!cc0N0kjX&2F4f6}0(lp(kM2V{HEG7-par+Pxoo zX|L)4ySsqs{v?^-Wuy~VJ4{M5m|BZ4jI2f%B847xIa)6S>%SZKm8)u-|;B@ z0#l53w%wd=hD9-ySBUKvBV9)8yXeeUaoHKpbS+@3Q%Po zR00pcma4oOu$e8Y-=Pw8rsR&Y=V8%6u<|NXC4r%c7a@6r1zez2Rr;I+fCZ3~y~w95 zc#%)V_(VgMs~3SRjm~`zF^jr%+eKZdwN0f;EUUV6wlxFWH$jkncm%os28+y~^fXPs zKuK5f`#j-e(9a?NoPnF$wmZtQY<_2M7Dx)$KOp=o$s)NbFUBa@{qCFiaplRvu{q5> z3QnBgp1>SjsI>`x>C(+e_Ry4>X>8P=V!fMAG{j%*G+p;q*fv3ETK`yNrnu`|Q4?a_I5CZT%6AZXOs$5O*jPe<%6%YyF zkd>Z$uGJqZK(fgpwlT8>|JBX4Jkk$_sYI6wieyI8UDMF+;nnGpuFi2IQMgt({D)`fLL>S_{@NaC{pj+#=3B!HWcJiFdF#b%i2 zB`tGPF)8WzK|XI7Q5TF9WE?#h+5OZkOhBhP01Ab{O0B?{L=Dx+ z+3%Kbt!+3D*T1YN(M^=Kj~yc?jL$-%wMHHG!^s^{FFcQ5D>i* zqPg%7ME^>%6FkS}x4abtHcGLwm%N?=RAK+C&O`Z7DNrKW#1OPbLm?q4-G4Z7*LKwa zvdv$GW&I~u70`kY%VxR60w1$3eN{OnVf9C(m z3XI-+0cD6zgg`Dq=*iH@*NvaP@A+gg{&9G>#Y_}yH$$McygIX1YowE2EQJ9wz%z+f<2)(PDf2~L>_ZAFx z8yOKxL_bVifYQE*(qRbWGeR2Cw$g2NPtE>51qON-HbNrb2S?oxn6UtIzVLDpZHM#` zv_NCXXX1WK-^|*9Q`r=z)z9i42||46YytRpx;;+NJ7~Q5&2yBn&NzDo{Kp&rlZZ$$ zA&#Ko^N#Z{q4)$!5K7*XJb+8Dz(?O{{Uw5wIgK1{f&Z`}?P+q_(}bwajlW@*n3-=a z@IUc6&>{`tZ6_R^I1!pk^NPzpOI9nm*$EpOPJP29#1bp^;n zrZU0!|7|Psdwc~?h#(h=sh~25%-Bq%MQMMwGJCWTW>@8SEs6&i8*6#01U!jk^a~8rh=##R=nxhii7$~)1Tj$6Sd(+rkP1yB77%lavopL2)$+!W z*A*LMG(}FY#@t5)CtUNAuC7VnR#c92jTdkd6r9zyTX|4BRZL2zeBmsb^4#Fia9)y? zugnAFS?Gg^e%>hhDH!WD+{80D28PrtCi#)Hwt4Phn|>!w~<5^w|gdF@nZHCrNAB zVe}8ziwL5ZU>@i(LY$TTAp}4qf8ucfms)8ZJw656Z41!{^#?_UCq=99=r^Wgy(Pazj;Ks=7W)|I<+1V|%ygJ3{$#0cOz; z|6g0*0Z--o{{I|1$%tdGQq+;Xl66W(GPA-l3LP_hha-KEE!i`&H-`vCwquiVK0;QM zk&#Wm`}Fya-|zMRKd;Vt&h~WQ@9Vzq`?}xP`%=reN>LoIr>rj{`>K*z;HAF$t{U=d zTsyUR=VkGZtME%tmUCEDr2|}xqKhanXVfBD(u=G>znHi4yLFA ztePK_eyt;$gnq9d8h))L!32H1R}edim-K`C3B1lW)yCgRwX`A~oie6J0!z>!s%0$B zd6{R2D7LY?uKzHNVQd(`^Tl4OC+?;PjdbTl71|q%?`%g&n-%Sn9WTxha&ocdn0Ih&8bL@MG`Gk1#z}8)+RA7fRVH`?J z8v;%so!|sApL!frz`6dh4RK>hprzr`Zdz0;*Mn^}$h~Ceh@#fj>~~QgtxOgNx&guo zu5VjbI!hcxdUSbR*wpGf4PMm*tJ;FrBC3+mm$<9ep z68IQUdg_u&6!9Hoac}oU6CUJ&Q^GI&c6%vg&${` zLFY)??efkkFD};-TP+8_n8q=KM!BjD`b!>71$DYS8%)N%MGx1O;B_EBdkH-816a}3 zufeOE3WKc`CH)+x2jRn#*Gr4-P3RtrmF^nb<_x6{v`>z=PqId_hh7S)TRELRAS`58 zo2Msd4Xe2PzPcUj9=d$@5AEMG`r^ ziU(cFDHqt`8v}N|#1Ol(0l%RL4=(AG-2U2wSvXhopYiUxrhZ0oZ%w%l4DclxSEa6Y znBi)~TwPc}_@UFP;emxCVyo2JOd=(SkXsNUAX3`LK#3g?un4QX9+1Br5J>B^pRiIf-aAl#ug%C>^+w>am_ME1G{P;Fq+ zdaSpm#yW~-S=$q=Af#qrutnrq*ZkAny+!&S z<0Pc9{lFdK)BTLgH)ugLt-#+2MGr!-57kpGjgpl|fSAR_ zVynKE??9sj7p+m%^D^k2*ZUCvneU91F*&pKWtVHed)~^3O3)v!sxHDx_{mjItw{jF z@q?1b74=K#n%4%eiVa+giB%gwZVj_UNe5vADNaK#93*Qp-3`rUy+Ev}l07mkr1VAt z>~rTS-?2m3j?mv%RAB&u5*RM_ahLCd*S3G2tn6-W$GrQQa+W4$4QZ8kL&P6j%inKE zjek)5BnZxvB3^1&I75;tfVdY>E8j?FGG^9;DMk8pWx*(Wqm1-58b2$q)OBl2XCZ? z7vgyR@i&SEo?bHfnuu3Vj~5V6sgY1IkQPso=CwTXQlPw1PMXy6?;6iDbPW!<-Bc`~pJ zra8qWHzvQu;&|Cpq~JPiGX7p#1g8?XgnSv9(}_j)8U9IR^$fXwYH*ju-)JFG(!K5kXZkLII@sIac(p5BN$4@R7@ExNL zupJY>ngh2*+0=hSfB%LMiRPgvHJw867>)_RtL}Dc#qG+*%c4KX6hyjprQAEBx#08N zy~N%NUGw=YbvcVTQ>uSYHg?yFO~~@dDQQh%bwDG)zLUPMThiS6>&cV{hah{(;l%yW zRt1&04G`jZxQ1L6eZ$^>y1Ah{S7)j(czmnbk6#@*8ri`M+*0J|IyT!NoOmTZz zOVc0iy$AVf-xNd~1`6Q=leUwaGCOsrPrCRk>>JR>Kh{6KmFAM0Z|2i#!)3j+3^CVu z(O_yWStMdQF0|8C%*8Z(?AAK=)is@^N3eM#Wj$1DYoX_p@9E9-G}fcaL)-Px`&(_- z_Yr>P!_WLa5)ZEahF-HCJ;wFy)Zk}APVD6?@E@Xgr{iOUe*BpJ;H7nvJ<)x9e=tTW z>X*+~ub6<3=L%A0Pz}?EqP>Spo&ystP21SNzp$c4-OymfiM}2ssJ@}vz`NL_Z)6qm z%2A`tqEW}zQ8F13y?%KyEiQcKNl{v+m4AYTmNkwHPqh(7P{Ny4)9~ zaRBAv>5={P95r!nQdEn-P>a8w*jUz~zx(g3y9_8E-JQ3rXdEkl>hxCV?XR23D-D+y z+XR(;TyN-gPCe<8om%Q1yzzHGH&AwP8=b}u=ax4iG`H;(i*4XGBn zRKJO<`_d=q#%syD9u>RRh>NUR_J6O)`{h%UH1gte21bwxsa?#ZZ)D^ zzq>xUo-(3??&0%lo)5Q=Vu%&04A2eCG{!`k7D#{dpxF)PlbPUwpKPa{&t@W`04t>O zt$T|0VBt}ED%j>o|DYy2N34$dzoXoLp2439)%rhumN~rJEV1vhW#qAkKQ2D`fj9pZ z!X+CR@4oTv^V$(J4Eq8WB)ml>AD^?_w*IN>j`Wj*?JOGwhMzMExm{^oHLtq#We+}t zmi=%(maaZ~1lA;UEMr(Pj##kC>7vKmp@+|veOFzp&p>bIcq{yk-e^%VT&cJyCvnu~ zD(Tzn+H7nZh@8eyS4k6L+i95;2Zz>oKBqhvqVoBDx*K|zCO3-OyR4P~Db#siceAKf zu2{dCZx>vQNEKAC9-L`1 z4s((wACY$LXNsL`GEI7|>2+c8m(rr<`Ht0rKz&Uo!vyJO{oVcbd#mZ3ZTpK8-So*rOkCq&w|w^^2riW#V1;JFo=B2Pr+;0e^X=8 zPcBpJZS|DgrXfi8T3LF!QM;BScoOK#6`HGga&_A50<=nJCIP+Fesu*Z4{tX#yo&0oP-FN-I=T9XX@`gnh0*eaC6!c;C$-Azb zA0oPLtp&_zoQWDTW`3)kHqDgid7YINEgjhT@xVLv84Y~Dv=?Ewrm6GIzyqGA6fV}2 zVgDMO3QG!$FDK|L=}1WE!}Y*ELHzo5b4yB_4elQjiNvgq`x@ zv$O%E!$~ZYQv=f#e1st*90=*C*vshOzmZ~XXn!L$?nOOLm= z(U04l(=zB^SBI_JUn;eg_SagqXA`H76jb!Vc`|Fe1al2D9>*oH?O&eg@Fp#q#9}_v z!z*t(E0U@IZ0DQW;wqZidBLsuh6k_NuG=v@$=q8ZhO+f5~ z-`Y-qA8D;i(5h;n7+*B)d#YH^iciy-HV7L5vnh`BZnm{0OHsXUgAr_J!?K1*HwW`# zQT(}ATiu3Q@O`XhtCxww{hzC+VNcZS$TAlhAR5EgiajG>%Kd;9mKi9O_wb$PkI9e~ zilHyZHsBwj*ZwnQ{cjf%9>D`V*3n1rBEALh8lP<9H-DMaoKk1VZM&Z5;AQ~Y~BaEZb<`szVapbCYkr;Y-hNW8PbISzfP@9liyG?U57K; z*~_|fV5{lqrfX~F!^TZE+4^QC719ot#~ZkpiKzRz;q!yn)^JgoC`X&cstgb7&aNAX z_DF=i$zMvHSkWR|7Xiw+JR2-&PL18rkO+=pwK=Rx*=nSH1;oI8H4}7o9|NUDRj_%1 zj3Tv(yMHG|8wh=X2yijK@jzz+oJ9j$b<5yQMk@NX<7=hNrKikGa@|#BVKp;j5>(Y; zH5#rRlKy29rPv#0e{~od&AL-o(!M{#yqk{D95Zb5CTqR$ysj{+SQ}M9SBtcbP`76> zjDinG*+)>Z&0P?Qa*5os4=WRix`K{CDM{1Zfo>LQ`;i)_DGw(Q_0}|S>L=tN7SpRW z3VVaVOfJ1zsat6}q9G&Cm8==0!#Zz}HFt?x*1RV4E6A&T6>JM8<&v(!K#Sd-1SYt)uas8Bj~b5PI_e zi5Nitgy&0kK03(FjQtE8I=%Q&b`N)|1@a(NcAN^n;7X=8+paI#(TWml2B>M8r`_H$ z8?pQ{<`PH)_B9!|J_L8U%t|r+oJepEaO)i01XHGFa`=MMyq7<#6B{n~`IZ>&<%*fO zb*Ifv^}KZw{Rp8$@8|Xty4Wm6bL;Y!L~buGVmJ{cA7dyer0f+s>_fvZ$SCc6B=@Y| zXsM+sZqZEWu66sb>bYICw;bzYh2R!^!PhHzsDeCan-e`8-IePtN9RqgI9PxAt69`n zbQ0w@J4YUV9rqX0U+%7+N%r|$W7CM0<@)?CjSePaWmZ;u7o`sUb0wA79JLCiKdP#4 zj2=ej=FF#se{U$&OheJ*-;dpA&z0d|Vn7+NPRKvZpP!jei!dIqtk1n+D#d_eGDK~k zXY+)2cz7fR-w)F@uU{+E?3|@ryk)|F-%d~$@Pj7BUoWArud((v*`G6hq~x(ReqhK8 zAMUz63b4X%w^dlSRpwIu%pPS^_9#bR?M>WTEWb_ykYX9U)%$sQ@DgcZ@ADim!j-v( zRo#UbS7OgiMV*7@F6ygm%NpP56hyL0Q!A+wiMNW1cT%pd%2&&zv$J?w^B22mRe$)_ zcA|B^U}V9k=bn9K%-hb>;*|Saejg3N9{4pyBD)hUvoe*JD_~|uquAPX$XU|9;#0MH zTvp@pMOKlfPDb5nfc52}j61tqMcaCKh9{T9i7wNl(KiLMKb$|J=Y@>ABx)|W!@fzf z5C)F_726=73@m7gfvnpQJiu$cpb`I+CXuWENFZk5^xeMSz0s4(Gmp^%E-A27|0w(B z^>zs9mMCP4+flDR!P9VcY;7zq9LoOa{MDqwx%P86S|)hM>{ejF?CDD7LX)-kmn4#D z&f6W{!`@%sk<5sxkfCgvo*s8bw7-`;iVGcHHM?SVb$EHHZDrtR zvrKzh$I5$IT-!=S+sZE?N++5(h1;7nIZiaWqfOVF?&tP3$%i$`$2{2L{dvS5n&9nE z=Pk!s_H|d|>#mt%PZMWY6K9Os&zzqXmln;khBHkSawowza6vvaY=Yo9ipElq=B^oy zasv(PPXiz4*{Zdr)|Ej)8l?#E0oItDsx|PYkF&2tw~rUeH2*LzND6#c0o!Og5ybSD z4}VOp{)G#_{>+|OcK^?jB$of%dQ+w1t1Bye6WZKXW^^RfM)bI}k zPjlw~u1n}}A7`SSS=cyVQZZ2{>3dOLap1OTfe1-=nGn072s|;Q3eaq7GQCtE_a|6m&EjW zVOvUyuu?`6Q1sseE)}z`680NXJ}?v#R4~cL0e?mu% znQOY&WEOJ=ydW4O55`#VD!{+Zj?+cad0(%WEY9W0J3>r3PE5HYO=e+DW+5?Rf{Z?3 z$W^aTnYlXVcV;M} zBL-J33V#b1MJTEI5`mS{E=853cx5S>w6dpEl@zVpWLiYdd_-8J;8i?+UPjvzFJt1F z-p;HJ7jeYA{~>*SeGQ7(^VpNiQ#66jUOK06RJuSHzug zOk}~YB7TK|qJYNSoz)3|_glns@?y*9*T_elPYo2V@&$jdI=tRL_6OzvX8`5v4V>p& ziDmco3wgUpm9-6=Z4gH49 z?-zUvxXLpA8~QYy&>>e=Dj@Ht{`9JglZ!?>^RzC0X5KWrt5%BfG#AT$nm4&ppJXF_ zOm3NrZM;3`qR~3+!XGAGes0YJMLEZlIF}Y&JZFkoTI28hzQ&J{76OxiipoZS;ohxA zAr>=Xms1uqDeG1#Qg$x@=D(daTG$Nszqwc)8);L@$kOek*%gt`FxX9 zPY}tq>(XJoj~iZg7s1s4DnwB2^p{p$xoj>g+3L~{@hRRP8WO9LlmLK}ghx3D4q3Wr zbTqd&-VU`{C*Z3MyDZSCVk5dPKi*s*LI#F#dl7j6H~%s#j_j8&_pr(7?JRp)_L`&X zWUdG$+XqBedZCwANJ6ae{kkSo3VUw@aKpL+EP88 z%=%iKtEfhkIs)XiA!Q&uW_&v{)L!EPvEVR%R1t~8dSZu$MA!Kv92tW`ZBZuE_p!=M z7uTKerl2hvIXDQ2`)hJ#U2w>4nsRQXKB1lc^yQlS9zbCEx%yk+Mx6QwacLN7K>$H8 zmeS!S(>j0D^UhJ5bPftiIX!*~eFcN#)4w~-m2+cOx`yAMMN_w_a1AFcc%HHbM6w&c zl==sBoc{Al^Si#%E;$6G0gw=Y#`;}$y57jTZDn_pRdimQ0HVu3n`P@$mO9_d_SHW- zZJkYGdLNRbGTNtAg>64Zr9PVErH&q+q{>GmW0Q-4bL7L1KmO0yffkWw!WV_{Ds{UG zBGo&9qjV+m)2j7lC9%OaUmy}U5FfT-*-NrF1#L7^UpH*Syca!==g98Dde*MX5etq2 zjQs-%87h$P5(QA1+?@&tuLHgA@D2?6O@68Xk+^A@ zxH#4E{o4GHpe$R@TCl9EPw1!%Z+QePW%!Y2a3<*P{Q0!7X#3|+sY}A2axa7Thn>Ml zQ%*X$x-Qt_w#Q_dTj?2`Rq1(4MyZ{>`7dYpX*GZb!smlL|G3vi!E^N#^8{4jo&$K` zq7qSBcjg9fF!)SF$~ZAUjN9;x9VnhG2+>M35K=mo?W@q0pKTQ1Hn=^^7?d@}-4ui#!Y3+*MerJd|?VTt0)!bF5(ZsHNil3|!LnlO%Sz@eK!EMEu8aMfY~VBhw9bDpyv~vR zY2J|WG!IYh7q&*Q?N6W+bLI_cUBNfp!Ho{*tkgO|a0On__j=Nx)oyWuE%=&+{B<`x zj-bx(E64n&Ou~h0;IS}iE)OhMAQ?hZ)j^O6MAi)!wr%%XL?Da>L~H?Dqa`MRTDaMu zI=0(IYqPwO8oo!7Yl$3pXlM8K!&A3a0z@R2Nr zC7WnoInIbIPI)R9+<@{<(So27f%i~K{k&YwR8Roe1CIrknQO3TehzwOK$AgaNnLBKfi>2S0lp(>^?? z%5Vp4nE5K!K3t8WrqTG|m#mj(MlrzY_ijmY2xaaq2Vz`*L!1izYiCBG8xv9M*0{2t zngN_?(cf?zy_QSO1|?iK?`yU<#XR{9DS{sh+^Nu5@)L6XaK3dm`k}SpgN4`ogFg(5 z>o#u~R&vo_(S}HNca9BpIkPw{zXS+mWgNs92#t5|9!fwIPQ=KfSXzi0Ea+H;p-7NO z3DI6-5QZRC5s1>_qJ=gnfO7Y75G6z_avP+boMY7nTKbAe0`sJ@)X0V|XHr5cZLyIe&L9i~a~Odk>!23ZYqTJDme5TkFeT)lyjhUs z!=ebwf!Bl*c6~?y!vx< z$2<5758t1q1Neg8XaM)&jV85UGcuoD76Pm1IxQoA&3<^FNgjOMoV|7ZN8oQr%YWRG z-Ckw~BGCoGuZ@q6C#u9dF(=ADp51S2G7pZ=`5}Ga?c=<^5MrRY>VLNRLH@XF4 z{}3)gSrr3?_knveOivUn*4q2)xBP9+Rz*otQ!&AVVq!7~G&UXC3P{F8K@Qr)Ru&d@ z+>~kpFwP)CQZ1xMiS#3Z_9$Q@pkdgMz$!-|46qFK_I`Zu&`qA8UL4c zzOX?M+B@Z8m+C|1$B zs?wlU4Ti)>{)@a#NyG+76kvo?p-d*8f`|yMNPNu#Vjyc}hsZix9zd`)%!L$oNRl#W zZN$RwU+qMckGyL^F?XImON5C=XE&gP#{HQ}8P$S9?yumL&A(a67Una|ZEn`AeTZ1y z@4`hJ^yF#@z4mPqiuq_##DKlhHk#qp<5sqt712&p?9kjZu2rR5(zS2jI$I{4a9|xs zzv$4s`uWkG?nyz+=ehl*r{#lZzaUn473$~ z&24@5BT0-`jZ%md1PL&^Iy3fN}{2WYMS`DqS8*=7|^ zjs}greanx2xIGj6kA-FflvM5!=hU~^k7x8H<9YI)kLAtz#$O%Fsh~>+Ii=nxOpmJ( zH45QngmQJ}-;13NSyEMPEXR%V=D4&OnLHamn5EsK`}~o0u$Pe=mOJ_l%Pqt+3G(uY zh)HXJtuup`g{bLTgh6sOj2jZE$^pF@V~u2SpoADZTR=qMPwqSf0?wkyBA^9Jnpcgj zBL`c135EP1SU)KQlzrEjt|<@N2?fk$9Al!hi3-7|fy{s&HxUSAVjvPWpvew&{u}@z zNJQlT0RA~BRdOIJN%Be{+0G`A41=|hA*n|Eh)IDs062VU9bhbwme6vM?8nr-6NSys zd^MN0U`NVi7?9ryQJjH0_Due^7@Z;4Eo-T0?`ekspEYs~UvsYSH>-ELjy`Um@)z!J zP4v2(@-3c?yP_k{Dg;GOH~FH^0pc7mXKo>;T0`4Gii2!MrpuQN3>nxC5 zEUCjN6;&M)ICW47BijKCfZ)R<#DtF`tN^^(7#N2bipGZEk`X!y$Um$XQsa#L#WYZX zLvpabG3X!G(*;jI>uLX%(SW(=O=t34ByTX`7X(uEz(AS}gX?R6;g^aQcu6wt77(?Y zB*y%cQV-q)>BZn^Va56cpLsDD}-+kNJjXY;7J!h+HFQ{}l{?Lt}`q`jyZ_6?I z)+)cj^Lu}$dKUgUWwNYMg)-hvAJzX3ov zTM2s@cvXk9tnc*3 zZcvqxGd`)n@?ev_fe#I(aG(llf%70poH5r5p9euC04*w{H?1tF=`avx6(YTz09jLk zh8GAUgMhv^c;rw>Oa$)1qnk(u+ezZL5Of{(T$mf6`7N0V37M@Nmz98<#7E?BENF3lMouHuO9QY|SDe6XGdQ z&p$?jFS~o*Xi0O-TIJyje44U8WBIM|Ttu4M7=BE#*3Pp4VWLrXSH>VO!7^UGgx@&K zBTPxdR5Jm)a;+Gf3JU!c8t|5h(`|9l=iU93T@kCZkBAe|Sqga8dO{W?ejWnGdtFEM zw(5gy$@3Nrpvnlipf_)qz#ITf_)UmOpl%4EqDoDvR7JHHq3cT4$cCPFx}=r$?{4N+Bt!gr%|m*Tf*P)u6z(R_cmADDTv{ zXxbX}6coe(Bq4SCYM?VL6U^q|C9=UWFS#MV%+Z@dY#X;PvLPKSnc!NPB4~91R9@j# zOCk2ra>!SyBs8o3H9`HmYOaM-a)PRSk=00R z9b4;H0<2*KXJk;4XJI_>**LO9hx&7|Yw5ayK8t>sXPIt-MuDzLm}fYs5%Aq!hq~%q zt<-cg-?g&#s|ca6VZF~|LrMq_32ojC?#qNo!9J<|G=qQ68+IS_Og$1mn6Bp75W`=2 zRn!HK$M6KGE4Q@sU<&KQ{78w|h)Drp2ul5BsaK_uEh6fSo$U-|=duNWTP$_!W=gc# zv<5f2Q86>=`ejGJ5Aa@WKusD<4jZpI3b*VbVQUVUf~snW8o2T@1_#D=I_+d%k^;echo|{YTP&Qu-B9w<}5bKS)%hAI~m<+u)V46GsV=M~1w0_7?s8;vRL$gT7f}Aq zy8P)~XE^@)&2mZ=2$nM(q)KohM9*o7pj`Z;a6dN97Us)zinfzj9XkQW{4ug#;Pr@s z<11|7E&(jK9?6K%1a#t^G2^oK^RGm^0UJ~`R25J`eXNYE1{TL}Dbd`B0~O~!cg&5} zAE!8)2&W2JPetcD#D0sV;Bw;$jN6ON7JilB=Z{$J_)Eq~4JKX+Q1X=ND7Y=Edbky`)Z< zm+n)~*7q@!{PcAfVZ!Ue7B*jug`MLWIt7dcyfyQ z`JpyZOr1bN-n)#$1#B5yGVD)Dlpt%JM8z8^=-5!edNvgD@#6lw;Xx%RYN@=E zS09Wr7-W1^oT5`*2-heu!oLc(w81)(Ri~F$m0fOWP#z^MyH=ZN#HpWXL?PM};zI@c z@iC`S4d5Wg8cc*=aSQ>XHNso1EQlEI(O)N>x1g@el&*ei5wAm1Zgcu%(+`i%t zW=`#Fmy|J#t=xY6_awtT0W({)n|f?I4iwaZdtE`rC#p6e+Qk9eFyfVHk8zl3cNuzG zKxDj@#lS}=QGNT*u$*C?sFLw0;prtj*7nP$Ky?b|1l!yMnqQ#AHnJaX=LxE$ib&XA zUk4f5BANWy#uUIR5lPAYGxl-xM7tU6TwRDywQ{@CC0$Yj`f}QPag;rteVM~eGI@C+ zrTj_cLfsSp0i%@tm(k^4Mcw=_=RbBd8fP9n+tT%aOL;joWp#28f=v6O~ zl=HD=PwzFMXaYba{GC8VVS#WVQP1Y*+_kUc~xbpJ=lpFeK@Hh+Hp(& z8ja!|c+hPN{}+wI>|7ae1`?LV$U+Lyfhrs~%M&A$&FhsgZ)`g$>)Zq_+>q6Q^Z~XE zM)LOt+UDG~)C^2mDWF?W7GUwjiZ`ks^~6Ho1|_AfAe{WBpZYaumSQ#<;?Jcr?z12& zS5k$FLm}cR2Z$dDS-{jG5);@RZdz#?B$S6FD$3^8&(wp^z~qpZYXCzK!`cpN3&INR zF3I)uV#*U%XY$`K}? zzj4e9gGniN^~Hqnv!9+h@>)=gvL{SKRiv&;+0=2QH5lsJ7kE9=(1#&DdU#^z&4>3| zSUtk@!edDtA23t0hXTP18;GCIWgn9oe-*@Kd2ShU-JK+|v-OOK$tEkW!7-p9(}%Ct TIX+