diff --git a/gradle.properties b/gradle.properties index 24e10fd..a679c2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.20.1+build.10 loader_version=0.16.5 # Mod Properties -mod_version=1.8.5 +mod_version=1.9.0 maven_group=cc.reconnected archives_base_name=rcc-server diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index a025cd6..09a48b3 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -2,11 +2,14 @@ package cc.reconnected.server; import cc.reconnected.server.commands.RccCommand; import cc.reconnected.server.database.PlayerData; +import cc.reconnected.server.events.PlayerActivityEvents; import cc.reconnected.server.events.PlayerWelcome; import cc.reconnected.server.events.Ready; import cc.reconnected.server.http.ServiceServer; +import cc.reconnected.server.trackers.AfkTracker; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; @@ -48,6 +51,11 @@ public class RccServer implements ModInitializer { return luckPerms; } + private AfkTracker afkTracker; + public AfkTracker afkTracker() { + return afkTracker; + } + public static float getTPS() { return currentTps; } @@ -79,6 +87,7 @@ public class RccServer implements ModInitializer { ServerLifecycleEvents.SERVER_STARTED.register(server -> { luckPerms = LuckPermsProvider.get(); + afkTracker = new AfkTracker(); Ready.READY.invoker().ready(server, luckPerms); }); @@ -114,6 +123,7 @@ public class RccServer implements ModInitializer { ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { currentPlayerCount = server.getCurrentPlayerCount() - 1; }); + } public void broadcastMessage(MinecraftServer server, Text message) { diff --git a/src/main/java/cc/reconnected/server/events/PlayerActivityEvents.java b/src/main/java/cc/reconnected/server/events/PlayerActivityEvents.java new file mode 100644 index 0000000..7629e2e --- /dev/null +++ b/src/main/java/cc/reconnected/server/events/PlayerActivityEvents.java @@ -0,0 +1,31 @@ +package cc.reconnected.server.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; + +public final class PlayerActivityEvents { + + public static final Event AFK = EventFactory.createArrayBacked(PlayerActivityEvents.Afk.class, callbacks -> (handler, server) -> { + for (PlayerActivityEvents.Afk callback : callbacks) { + callback.onAfk(handler, server); + } + }); + + public static final Event AFK_RETURN = EventFactory.createArrayBacked(PlayerActivityEvents.AfkReturn.class, callbacks -> (handler, server) -> { + for (PlayerActivityEvents.AfkReturn callback : callbacks) { + callback.onAfkReturn(handler, server); + } + }); + + @FunctionalInterface + public interface Afk { + void onAfk(ServerPlayerEntity player, MinecraftServer server); + } + + @FunctionalInterface + public interface AfkReturn { + void onAfkReturn(ServerPlayerEntity player, MinecraftServer server); + } +} diff --git a/src/main/java/cc/reconnected/server/trackers/AfkTracker.java b/src/main/java/cc/reconnected/server/trackers/AfkTracker.java new file mode 100644 index 0000000..b62bdd4 --- /dev/null +++ b/src/main/java/cc/reconnected/server/trackers/AfkTracker.java @@ -0,0 +1,144 @@ +package cc.reconnected.server.trackers; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.events.PlayerActivityEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.player.*; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.TypedActionResult; + +import java.util.HashMap; +import java.util.UUID; + +public class AfkTracker { + private static final int cycleDelay = 10; + private static final int absentTimeTrigger = 300 * 20; // 5 mins (* 20 ticks) + private final HashMap playerPositions = new HashMap<>(); + private final HashMap playerLastUpdate = new HashMap<>(); + private final HashMap playerAfkStates = new HashMap<>(); + + public AfkTracker() { + ServerTickEvents.END_SERVER_TICK.register(server -> { + if (server.getTicks() % cycleDelay == 0) { + updatePlayers(server); + } + }); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + final var player = handler.getPlayer(); + var playerPosition = new PlayerPosition(player); + playerPositions.put(player.getUuid(), playerPosition); + playerLastUpdate.put(player.getUuid(), server.getTicks()); + playerAfkStates.put(player.getUuid(), false); + }); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + playerPositions.remove(handler.getPlayer().getUuid()); + playerLastUpdate.remove(handler.getPlayer().getUuid()); + playerAfkStates.remove(handler.getPlayer().getUuid()); + }); + + AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> { + resetAfkState((ServerPlayerEntity) player, world.getServer()); + return ActionResult.PASS; + }); + + AttackEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + resetAfkState((ServerPlayerEntity) player, world.getServer()); + return ActionResult.PASS; + }); + + UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> { + resetAfkState((ServerPlayerEntity) player, world.getServer()); + return ActionResult.PASS; + }); + + UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + resetAfkState((ServerPlayerEntity) player, world.getServer()); + return ActionResult.PASS; + }); + + UseItemCallback.EVENT.register((player, world, hand) -> { + resetAfkState((ServerPlayerEntity) player, world.getServer()); + return TypedActionResult.pass(player.getStackInHand(hand)); + }); + + ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) -> { + resetAfkState(sender, sender.getServer()); + return true; + }); + + ServerMessageEvents.ALLOW_COMMAND_MESSAGE.register((message, source, params) -> { + if(!source.isExecutedByPlayer()) + return true; + resetAfkState(source.getPlayer(), source.getServer()); + return true; + }); + } + + public void updatePlayers(MinecraftServer server) { + var players = server.getPlayerManager().getPlayerList(); + var currentTick = server.getTicks(); + players.forEach(player -> { + if (!playerPositions.containsKey(player.getUuid())) { + playerPositions.put(player.getUuid(), new PlayerPosition(player)); + return; + } + var oldPosition = playerPositions.get(player.getUuid()); + var newPosition = new PlayerPosition(player); + if (!oldPosition.equals(newPosition)) { + playerPositions.put(player.getUuid(), newPosition); + resetAfkState(player, server); + return; + } + + if (playerAfkStates.get(player.getUuid())) { + return; + } + + if ((playerLastUpdate.get(player.getUuid()) + absentTimeTrigger) <= currentTick) { + // player is afk after 5 mins + playerAfkStates.put(player.getUuid(), true); + PlayerActivityEvents.AFK.invoker().onAfk(player, server); + } + }); + } + + private void resetAfkState(ServerPlayerEntity player, MinecraftServer server) { + playerLastUpdate.put(player.getUuid(), server.getTicks()); + if (playerAfkStates.get(player.getUuid())) { + PlayerActivityEvents.AFK_RETURN.invoker().onAfkReturn(player, server); + playerAfkStates.put(player.getUuid(), false); + } + } + + public static class PlayerPosition { + public String dimension; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + + + public boolean equals(PlayerPosition obj) { + return x == obj.x && y == obj.y && z == obj.z + && yaw == obj.yaw && pitch == obj.pitch + && dimension.equals(obj.dimension); + } + + public PlayerPosition(ServerPlayerEntity player) { + dimension = player.getWorld().getRegistryKey().getValue().toString(); + x = player.getX(); + y = player.getY(); + z = player.getZ(); + yaw = player.getYaw(); + pitch = player.getPitch(); + } + } +}