From 5b11ac03c4acf29692799cb4be5ea42c1d1a2f8a Mon Sep 17 00:00:00 2001
From: SakuraWald <sakurawald@gmail.com>
Date: Thu, 26 Oct 2023 16:57:33 +0800
Subject: [PATCH] refactor: rename ChatStyleModule -> ChatModule + refactor:
 Position.of() + add: ReplyModule + add: AfkModule + refactor: FlyModule +
 refactor: GodModule + refactor: rename ChatStyleModule -> ChatModule +

---
 .../github/sakurawald/config/ConfigGSON.java  | 16 +++-
 .../sakurawald/mixin/afk/PlayerListMixin.java | 21 +++++
 .../mixin/afk/ServerPlayerMixin.java          | 84 +++++++++++++++++++
 .../mixin/chat/PlayerListMixin.java           |  4 +-
 .../ServerGamePacketListenerImplMixin.java    |  4 +-
 .../teleport_warmup/ServerPlayerMixin.java    |  3 +-
 .../sakurawald/module/afk/AfkModule.java      | 83 ++++++++++++++++++
 .../module/afk/ServerPlayerAccessor_afk.java  | 12 +++
 .../sakurawald/module/back/BackModule.java    |  2 +-
 .../{ChatStyleModule.java => ChatModule.java} |  2 +-
 .../sakurawald/module/fly/FlyModule.java      | 13 ++-
 .../sakurawald/module/god/GodModule.java      | 13 +--
 .../resource_world/ResourceWorldManager.java  |  3 +-
 .../module/teleport_warmup/Position.java      | 13 +++
 .../github/sakurawald/util/MessageUtil.java   |  1 +
 .../assets/sakurawald/lang/en_us.json         |  7 +-
 .../assets/sakurawald/lang/zh_cn.json         |  7 +-
 src/main/resources/sakurawald.mixins.json     |  2 +
 18 files changed, 261 insertions(+), 29 deletions(-)
 create mode 100755 src/main/java/io/github/sakurawald/mixin/afk/PlayerListMixin.java
 create mode 100644 src/main/java/io/github/sakurawald/mixin/afk/ServerPlayerMixin.java
 create mode 100644 src/main/java/io/github/sakurawald/module/afk/AfkModule.java
 create mode 100644 src/main/java/io/github/sakurawald/module/afk/ServerPlayerAccessor_afk.java
 rename src/main/java/io/github/sakurawald/module/chat/{ChatStyleModule.java => ChatModule.java} (99%)

diff --git a/src/main/java/io/github/sakurawald/config/ConfigGSON.java b/src/main/java/io/github/sakurawald/config/ConfigGSON.java
index b9ccfce11..cee2d8abc 100755
--- a/src/main/java/io/github/sakurawald/config/ConfigGSON.java
+++ b/src/main/java/io/github/sakurawald/config/ConfigGSON.java
@@ -62,6 +62,8 @@ public class Modules {
         public Fly fly = new Fly();
         public God god = new God();
         public Language language = new Language();
+        public Reply reply = new Reply();
+        public Afk afk = new Afk();
 
         public class ResourceWorld {
             public boolean enable = false;
@@ -386,9 +388,21 @@ public class Language {
             public boolean enable = false;
         }
 
-        public Reply reply = new Reply();
         public class Reply {
             public boolean enable = false;
         }
+
+        public class Afk {
+
+            public boolean enable = false;
+            public String format = "<gray>[AFK] <reset>%player_display_name%";
+
+            public AfkChecker afk_checker = new AfkChecker();
+
+            public class AfkChecker {
+                public String cron = "* 0 0 ? * * *";
+                public boolean kick_player = false;
+            }
+        }
     }
 }
diff --git a/src/main/java/io/github/sakurawald/mixin/afk/PlayerListMixin.java b/src/main/java/io/github/sakurawald/mixin/afk/PlayerListMixin.java
new file mode 100755
index 000000000..d68a3dbe2
--- /dev/null
+++ b/src/main/java/io/github/sakurawald/mixin/afk/PlayerListMixin.java
@@ -0,0 +1,21 @@
+package io.github.sakurawald.mixin.afk;
+
+import io.github.sakurawald.module.afk.ServerPlayerAccessor_afk;
+import lombok.extern.slf4j.Slf4j;
+import net.minecraft.network.Connection;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.players.PlayerList;
+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(PlayerList.class)
+@Slf4j
+public abstract class PlayerListMixin {
+    @Inject(at = @At(value = "TAIL"), method = "placeNewPlayer")
+    private void $placeNewPlayer(Connection connection, ServerPlayer player, CallbackInfo info) {
+        ServerPlayerAccessor_afk afk_player = (ServerPlayerAccessor_afk) player;
+        afk_player.sakurawald$setLastLastActionTime(player.getLastActionTime());
+    }
+}
diff --git a/src/main/java/io/github/sakurawald/mixin/afk/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/mixin/afk/ServerPlayerMixin.java
new file mode 100644
index 000000000..a262f866a
--- /dev/null
+++ b/src/main/java/io/github/sakurawald/mixin/afk/ServerPlayerMixin.java
@@ -0,0 +1,84 @@
+package io.github.sakurawald.mixin.afk;
+
+import io.github.sakurawald.config.ConfigManager;
+import io.github.sakurawald.module.afk.ServerPlayerAccessor_afk;
+import io.github.sakurawald.util.MessageUtil;
+import lombok.extern.slf4j.Slf4j;
+import net.kyori.adventure.text.TextReplacementConfig;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import static io.github.sakurawald.util.MessageUtil.ofComponent;
+import static io.github.sakurawald.util.MessageUtil.toVomponent;
+
+@Mixin(ServerPlayer.class)
+@Slf4j
+public abstract class ServerPlayerMixin implements ServerPlayerAccessor_afk {
+
+    @Unique
+    private final ServerPlayer player = (ServerPlayer) (Object) this;
+    @Shadow
+    @Final
+    public MinecraftServer server;
+    @Unique
+    private boolean afk = false;
+
+    @Unique
+    private long lastLastActionTime = 0;
+
+    @Inject(method = "getTabListDisplayName", at = @At("HEAD"), cancellable = true)
+    public void getTabListDisplayName(CallbackInfoReturnable<Component> cir) {
+        ServerPlayerAccessor_afk accessor = (ServerPlayerAccessor_afk) player;
+
+        if (accessor.sakurawald$isAfk()) {
+            cir.setReturnValue(Component.literal("afk " + player.getGameProfile().getName()));
+            net.kyori.adventure.text.@NotNull Component component = ofComponent(ConfigManager.configWrapper.instance().modules.afk.format)
+                    .replaceText(TextReplacementConfig.builder().match("%player_display_name%").replacement(player.getDisplayName()).build());
+            cir.setReturnValue(toVomponent(component));
+        } else {
+            cir.setReturnValue(null);
+        }
+    }
+
+
+    @Inject(method = "resetLastActionTime", at = @At("HEAD"))
+    public void resetLastActionTime(CallbackInfo ci) {
+        if (sakurawald$isAfk()) {
+            sakurawald$setAfk(false);
+            MessageUtil.sendBroadcast("afk.off.broadcast", player.getGameProfile().getName());
+        }
+    }
+
+    @Override
+    public void sakurawald$setAfk(boolean flag) {
+        this.afk = flag;
+        this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, (ServerPlayer) (Object) this));
+    }
+
+    @Override
+    public boolean sakurawald$isAfk() {
+        return this.afk;
+    }
+
+    @Override
+    public void sakurawald$setLastLastActionTime(long lastActionTime) {
+        this.lastLastActionTime = lastActionTime;
+    }
+
+    @Override
+    public long sakurawald$getLastLastActionTime() {
+        return this.lastLastActionTime;
+    }
+
+}
diff --git a/src/main/java/io/github/sakurawald/mixin/chat/PlayerListMixin.java b/src/main/java/io/github/sakurawald/mixin/chat/PlayerListMixin.java
index e10c91ba0..0c799aa4c 100755
--- a/src/main/java/io/github/sakurawald/mixin/chat/PlayerListMixin.java
+++ b/src/main/java/io/github/sakurawald/mixin/chat/PlayerListMixin.java
@@ -1,7 +1,7 @@
 package io.github.sakurawald.mixin.chat;
 
 import io.github.sakurawald.module.ModuleManager;
-import io.github.sakurawald.module.chat.ChatStyleModule;
+import io.github.sakurawald.module.chat.ChatModule;
 import io.github.sakurawald.util.CarpetUtil;
 import net.minecraft.network.Connection;
 import net.minecraft.server.level.ServerPlayer;
@@ -16,7 +16,7 @@
 public abstract class PlayerListMixin {
 
     @Unique
-    private static final ChatStyleModule module = ModuleManager.getOrNewInstance(ChatStyleModule.class);
+    private static final ChatModule module = ModuleManager.getOrNewInstance(ChatModule.class);
 
     @Inject(at = @At(value = "TAIL"), method = "placeNewPlayer")
     private void $placeNewPlayer(Connection connection, ServerPlayer player, CallbackInfo info) {
diff --git a/src/main/java/io/github/sakurawald/mixin/chat/ServerGamePacketListenerImplMixin.java b/src/main/java/io/github/sakurawald/mixin/chat/ServerGamePacketListenerImplMixin.java
index 8138d421c..f821c3923 100755
--- a/src/main/java/io/github/sakurawald/mixin/chat/ServerGamePacketListenerImplMixin.java
+++ b/src/main/java/io/github/sakurawald/mixin/chat/ServerGamePacketListenerImplMixin.java
@@ -1,7 +1,7 @@
 package io.github.sakurawald.mixin.chat;
 
 import io.github.sakurawald.module.ModuleManager;
-import io.github.sakurawald.module.chat.ChatStyleModule;
+import io.github.sakurawald.module.chat.ChatModule;
 import net.minecraft.network.chat.PlayerChatMessage;
 import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.server.network.ServerGamePacketListenerImpl;
@@ -15,7 +15,7 @@
 @Mixin(value = ServerGamePacketListenerImpl.class, priority = 1001)
 public abstract class ServerGamePacketListenerImplMixin {
     @Unique
-    private static final ChatStyleModule module = ModuleManager.getOrNewInstance(ChatStyleModule.class);
+    private static final ChatModule module = ModuleManager.getOrNewInstance(ChatModule.class);
     @Shadow
     public ServerPlayer player;
 
diff --git a/src/main/java/io/github/sakurawald/mixin/teleport_warmup/ServerPlayerMixin.java b/src/main/java/io/github/sakurawald/mixin/teleport_warmup/ServerPlayerMixin.java
index 9c12b2fb4..692bc7857 100755
--- a/src/main/java/io/github/sakurawald/mixin/teleport_warmup/ServerPlayerMixin.java
+++ b/src/main/java/io/github/sakurawald/mixin/teleport_warmup/ServerPlayerMixin.java
@@ -42,8 +42,7 @@ public abstract class ServerPlayerMixin implements ServerPlayerAccessor {
         if (!module.tickets.containsKey(playerName)) {
             module.tickets.put(playerName,
                     new TeleportTicket(player
-                            , new Position(player.level(), player.position().x, player.position().y, player.position().z, player.getYRot(), player.getXRot())
-                            , new Position(targetWorld, x, y, z, yaw, pitch), false));
+                            , Position.of(player), new Position(targetWorld, x, y, z, yaw, pitch), false));
             ci.cancel();
             return;
         } else {
diff --git a/src/main/java/io/github/sakurawald/module/afk/AfkModule.java b/src/main/java/io/github/sakurawald/module/afk/AfkModule.java
new file mode 100644
index 000000000..3bd0c1df6
--- /dev/null
+++ b/src/main/java/io/github/sakurawald/module/afk/AfkModule.java
@@ -0,0 +1,83 @@
+package io.github.sakurawald.module.afk;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import io.github.sakurawald.ServerMain;
+import io.github.sakurawald.config.ConfigManager;
+import io.github.sakurawald.module.AbstractModule;
+import io.github.sakurawald.util.MessageUtil;
+import io.github.sakurawald.util.ScheduleUtil;
+import lombok.extern.slf4j.Slf4j;
+import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
+import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
+import net.minecraft.commands.CommandBuildContext;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+
+@Slf4j
+public class AfkModule extends AbstractModule {
+
+    @Override
+    public void onInitialize() {
+        CommandRegistrationCallback.EVENT.register(this::registerCommand);
+        ServerLifecycleEvents.SERVER_STARTED.register(this::registerScheduleTask);
+    }
+
+    public void registerCommand(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext registryAccess, Commands.CommandSelection environment) {
+        dispatcher.register(Commands.literal("afk").executes(this::$afk));
+    }
+
+    @SuppressWarnings("SameReturnValue")
+    private int $afk(CommandContext<CommandSourceStack> ctx) {
+        ServerPlayer player = ctx.getSource().getPlayer();
+        if (player == null) return Command.SINGLE_SUCCESS;
+
+        boolean flag = !((ServerPlayerAccessor_afk) player).sakurawald$isAfk();
+        ((ServerPlayerAccessor_afk) player).sakurawald$setAfk(flag);
+
+        MessageUtil.sendMessage(player, flag ? "afk.on" : "afk.off");
+        return Command.SINGLE_SUCCESS;
+    }
+
+    public void registerScheduleTask(MinecraftServer server) {
+        ScheduleUtil.addJob(AfkCheckerJob.class, ConfigManager.configWrapper.instance().modules.afk.afk_checker.cron, new JobDataMap());
+    }
+
+    public static class AfkCheckerJob implements Job {
+
+        @Override
+        public void execute(JobExecutionContext context) throws JobExecutionException {
+            for (ServerPlayer player : ServerMain.SERVER.getPlayerList().getPlayers()) {
+                ServerPlayerAccessor_afk afk_player = (ServerPlayerAccessor_afk) player;
+
+                // get last action time
+                long lastActionTime = player.getLastActionTime();
+                long lastLastActionTime = afk_player.sakurawald$getLastLastActionTime();
+                afk_player.sakurawald$setLastLastActionTime(lastActionTime);
+
+                // diff last action time
+                /* note:
+                when a player joins the server,
+                we'll set lastLastActionTime's initial value to Player#getLastActionTime(),
+                but there are a little difference even if you call Player#getLastActionTime() again
+                 */
+                if (lastActionTime - lastLastActionTime <= 3000) {
+                    if (afk_player.sakurawald$isAfk()) continue;
+
+                    afk_player.sakurawald$setAfk(true);
+                    MessageUtil.sendBroadcast("afk.on.broadcast", player.getGameProfile().getName());
+                    if (ConfigManager.configWrapper.instance().modules.afk.afk_checker.kick_player) {
+                        player.connection.disconnect(MessageUtil.ofVomponent(player, "afk.kick"));
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/io/github/sakurawald/module/afk/ServerPlayerAccessor_afk.java b/src/main/java/io/github/sakurawald/module/afk/ServerPlayerAccessor_afk.java
new file mode 100644
index 000000000..afac4553c
--- /dev/null
+++ b/src/main/java/io/github/sakurawald/module/afk/ServerPlayerAccessor_afk.java
@@ -0,0 +1,12 @@
+package io.github.sakurawald.module.afk;
+
+public interface ServerPlayerAccessor_afk {
+
+    void sakurawald$setAfk(boolean flag);
+
+    boolean sakurawald$isAfk();
+
+    void sakurawald$setLastLastActionTime(long lastActionTime);
+
+    long sakurawald$getLastLastActionTime();
+}
diff --git a/src/main/java/io/github/sakurawald/module/back/BackModule.java b/src/main/java/io/github/sakurawald/module/back/BackModule.java
index f2bd1d321..8ae69a794 100755
--- a/src/main/java/io/github/sakurawald/module/back/BackModule.java
+++ b/src/main/java/io/github/sakurawald/module/back/BackModule.java
@@ -55,7 +55,7 @@ public void updatePlayer(ServerPlayer player) {
                 || (player.level() == lastPos.getLevel() && player.position().distanceToSqr(lastPos.getX(), lastPos.getY(), lastPos.getZ()) > ignoreDistance * ignoreDistance)
         ) {
             player2lastPos.put(player.getGameProfile().getName(),
-                    new Position(player.level(), player.position().x, player.position().y, player.position().z, player.getYRot(), player.getXRot()));
+                    Position.of(player));
         }
     }
 
diff --git a/src/main/java/io/github/sakurawald/module/chat/ChatStyleModule.java b/src/main/java/io/github/sakurawald/module/chat/ChatModule.java
similarity index 99%
rename from src/main/java/io/github/sakurawald/module/chat/ChatStyleModule.java
rename to src/main/java/io/github/sakurawald/module/chat/ChatModule.java
index f38dbd11d..fd09ed9de 100755
--- a/src/main/java/io/github/sakurawald/module/chat/ChatStyleModule.java
+++ b/src/main/java/io/github/sakurawald/module/chat/ChatModule.java
@@ -45,7 +45,7 @@
 
 @SuppressWarnings("UnstableApiUsage")
 @Slf4j
-public class ChatStyleModule extends AbstractModule {
+public class ChatModule extends AbstractModule {
 
     private final MiniMessage miniMessage = MiniMessage.builder().build();
     private final MainStatsModule mainStatsModule = ModuleManager.getOrNewInstance(MainStatsModule.class);
diff --git a/src/main/java/io/github/sakurawald/module/fly/FlyModule.java b/src/main/java/io/github/sakurawald/module/fly/FlyModule.java
index a48d6d9c2..481a2038d 100644
--- a/src/main/java/io/github/sakurawald/module/fly/FlyModule.java
+++ b/src/main/java/io/github/sakurawald/module/fly/FlyModule.java
@@ -29,16 +29,15 @@ private int fly(CommandContext<CommandSourceStack> ctx) {
         ServerPlayer player = ctx.getSource().getPlayer();
         if (player == null) return Command.SINGLE_SUCCESS;
 
-        boolean flag = player.getAbilities().mayfly;
-        player.getAbilities().mayfly = !flag;
-        if (flag) {
+        boolean flag = !player.getAbilities().mayfly;
+        player.getAbilities().mayfly = flag;
+        player.onUpdateAbilities();
+
+        if (!flag) {
             player.getAbilities().flying = false;
-            MessageUtil.sendMessage(player, "fly.off");
-        } else {
-            MessageUtil.sendMessage(player, "fly.on");
         }
 
-        player.onUpdateAbilities();
+        MessageUtil.sendMessage(player, flag ? "fly.on" : "fly.off");
         return Command.SINGLE_SUCCESS;
     }
 
diff --git a/src/main/java/io/github/sakurawald/module/god/GodModule.java b/src/main/java/io/github/sakurawald/module/god/GodModule.java
index 6d44692e2..885c5e4cb 100644
--- a/src/main/java/io/github/sakurawald/module/god/GodModule.java
+++ b/src/main/java/io/github/sakurawald/module/god/GodModule.java
@@ -29,16 +29,11 @@ private int god(CommandContext<CommandSourceStack> ctx) {
         ServerPlayer player = ctx.getSource().getPlayer();
         if (player == null) return Command.SINGLE_SUCCESS;
 
-        boolean flag = player.getAbilities().invulnerable;
-        player.getAbilities().invulnerable = !flag;
-
-        if (flag) {
-            MessageUtil.sendMessage(player, "god.off");
-        } else {
-            MessageUtil.sendMessage(player, "god.on");
-        }
-
+        boolean flag = !player.getAbilities().invulnerable;
+        player.getAbilities().invulnerable = flag;
         player.onUpdateAbilities();
+
+        MessageUtil.sendMessage(player, flag ? "god.on" : "god.off");
         return Command.SINGLE_SUCCESS;
     }
 
diff --git a/src/main/java/io/github/sakurawald/module/resource_world/ResourceWorldManager.java b/src/main/java/io/github/sakurawald/module/resource_world/ResourceWorldManager.java
index 96c9d6e9d..2cd627250 100755
--- a/src/main/java/io/github/sakurawald/module/resource_world/ResourceWorldManager.java
+++ b/src/main/java/io/github/sakurawald/module/resource_world/ResourceWorldManager.java
@@ -72,8 +72,7 @@ private static void kickPlayers(ServerLevel world) {
             if (teleportWarmupModule != null) {
                 teleportWarmupModule.tickets.put(player.getGameProfile().getName(),
                         new TeleportTicket(player
-                                , new Position(world, player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot())
-                                , new Position(overworld, spawnPos.getX() + 0.5, spawnPos.getY() + 0.5, spawnPos.getZ() + 0.5, 0, 0)
+                                , Position.of(player), new Position(overworld, spawnPos.getX() + 0.5, spawnPos.getY() + 0.5, spawnPos.getZ() + 0.5, 0, 0)
                                 , true));
             }
             player.teleportTo(overworld, spawnPos.getX() + 0.5, spawnPos.getY(), spawnPos.getZ() + 0.5, overworld.getSharedSpawnAngle(), 0.0F);
diff --git a/src/main/java/io/github/sakurawald/module/teleport_warmup/Position.java b/src/main/java/io/github/sakurawald/module/teleport_warmup/Position.java
index 381cb780e..77ab60405 100755
--- a/src/main/java/io/github/sakurawald/module/teleport_warmup/Position.java
+++ b/src/main/java/io/github/sakurawald/module/teleport_warmup/Position.java
@@ -2,6 +2,7 @@
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import net.minecraft.server.level.ServerPlayer;
 import net.minecraft.world.level.Level;
 
 @Data
@@ -14,4 +15,16 @@ public class Position {
 
     private float yaw;
     private float pitch;
+
+    public static Position of(ServerPlayer player) {
+        return new Position(player.level(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
+    }
+
+    public double distanceToSqr(Position position) {
+        if (this.level != position.level) return Double.MAX_VALUE;
+        double x = this.x - position.x;
+        double y = this.y - position.y;
+        double z = this.z - position.z;
+        return x * x + y * y + z * z;
+    }
 }
diff --git a/src/main/java/io/github/sakurawald/util/MessageUtil.java b/src/main/java/io/github/sakurawald/util/MessageUtil.java
index c7132af07..c17982995 100755
--- a/src/main/java/io/github/sakurawald/util/MessageUtil.java
+++ b/src/main/java/io/github/sakurawald/util/MessageUtil.java
@@ -125,6 +125,7 @@ public static Component ofComponent(String str) {
         return miniMessage.deserialize(str);
     }
 
+    // todo: auto add keys in language
     public static net.minecraft.network.chat.Component ofVomponent(String str) {
         return toVomponent(ofComponent(str));
     }
diff --git a/src/main/resources/assets/sakurawald/lang/en_us.json b/src/main/resources/assets/sakurawald/lang/en_us.json
index 2cd42bc62..cba56e60b 100755
--- a/src/main/resources/assets/sakurawald/lang/en_us.json
+++ b/src/main/resources/assets/sakurawald/lang/en_us.json
@@ -140,5 +140,10 @@
   "fly.on": "<gold>Fly mode: on",
   "fly.off": "<gold>Fly mode: off",
   "god.on": "<gold>God mode: on",
-  "god.off": "<gold>God mode: off"
+  "god.off": "<gold>God mode: off",
+  "afk.on": "<gold>Afk mode: on",
+  "afk.off": "<gold>Afk mode: off",
+  "afk.on.broadcast": "<gold>Player %s is now afk",
+  "afk.off.broadcast": "<gold>Player %s is no longer afk",
+  "afk.kick": "<red>You have been kicked for being afk."
 }
\ No newline at end of file
diff --git a/src/main/resources/assets/sakurawald/lang/zh_cn.json b/src/main/resources/assets/sakurawald/lang/zh_cn.json
index 20849f365..d317e61cd 100755
--- a/src/main/resources/assets/sakurawald/lang/zh_cn.json
+++ b/src/main/resources/assets/sakurawald/lang/zh_cn.json
@@ -140,5 +140,10 @@
   "fly.on": "<gold>飞行模式: 启用",
   "fly.off": "<gold>飞行模式: 禁用",
   "god.on": "<gold>上帝模式: 启用",
-  "god.off": "<gold>上帝模式: 禁用"
+  "god.off": "<gold>上帝模式: 禁用",
+  "afk.on": "<gold>闲置模式: 启用",
+  "afk.off": "<gold>闲置模式: 禁用",
+  "afk.on.broadcast": "<gold>玩家 %s 暂时离开了",
+  "afk.off.broadcast": "<gold>玩家 %s 回来了",
+  "afk.kick": "<red>您已因长时间离开而被踢出"
 }
\ No newline at end of file
diff --git a/src/main/resources/sakurawald.mixins.json b/src/main/resources/sakurawald.mixins.json
index 533d8ca4b..32bb5c165 100755
--- a/src/main/resources/sakurawald.mixins.json
+++ b/src/main/resources/sakurawald.mixins.json
@@ -4,6 +4,8 @@
   "compatibilityLevel": "JAVA_17",
   "plugin": "io.github.sakurawald.mixin.MixinConfigPlugin",
   "mixins": [
+    "afk.PlayerListMixin",
+    "afk.ServerPlayerMixin",
     "back.ServerPlayerMixin",
     "better_fake_player.PlayerCommandMixin",
     "better_fake_player.PlayerListMixin",