From d07fade5abb4eadbdda3bd6101dbb5ec7114d1a0 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Sun, 13 Oct 2024 19:24:25 +0200 Subject: [PATCH] Add AFK and active time support --- build.gradle | 3 +- gradle.properties | 5 +- .../java/cc/reconnected/server/RccServer.java | 23 ++- .../server/RccServerConfigModel.java | 5 +- .../server/commands/AfkCommand.java | 32 ++++ .../server/commands/RccCommand.java | 2 - .../server/data/StateSaverAndLoader.java | 59 ++++++++ .../server/data/WorldPlayerData.java | 5 + .../server/database/PlayerData.java | 1 + .../server/trackers/AfkTracker.java | 139 +++++++++++++----- 10 files changed, 227 insertions(+), 47 deletions(-) create mode 100644 src/main/java/cc/reconnected/server/commands/AfkCommand.java create mode 100644 src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java create mode 100644 src/main/java/cc/reconnected/server/data/WorldPlayerData.java diff --git a/build.gradle b/build.gradle index f241b66..7f818cd 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ repositories { // for more information about repositories. maven { url 'https://maven.wispforest.io' } + maven { url 'https://maven.nucleoid.xyz' } } loom { @@ -44,7 +45,7 @@ dependencies { include "io.wispforest:owo-sentinel:${project.owo_version}" compileOnly "net.luckperms:api:${project.luckpermsapi_version}" - + include modImplementation("me.lucko:fabric-permissions-api:${project.permissions_api_version}") } processResources { diff --git a/gradle.properties b/gradle.properties index a6097cb..c93476f 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.9.1 +mod_version=1.9.2 maven_group=cc.reconnected archives_base_name=rcc-server @@ -18,4 +18,5 @@ fabric_version=0.92.2+1.20.1 owo_version=0.11.2+1.20 -luckpermsapi_version=5.4 \ No newline at end of file +luckpermsapi_version=5.4 +permissions_api_version=0.2-SNAPSHOT \ No newline at end of file diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index 09a48b3..e51c36e 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -1,5 +1,6 @@ package cc.reconnected.server; +import cc.reconnected.server.commands.AfkCommand; import cc.reconnected.server.commands.RccCommand; import cc.reconnected.server.database.PlayerData; import cc.reconnected.server.events.PlayerActivityEvents; @@ -9,13 +10,13 @@ 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; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPermsProvider; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; @@ -78,6 +79,7 @@ public class RccServer implements ModInitializer { LOGGER.info("Starting rcc-server"); CommandRegistrationCallback.EVENT.register(RccCommand::register); + CommandRegistrationCallback.EVENT.register(AfkCommand::register); try { serviceServer = new ServiceServer(); @@ -124,6 +126,13 @@ public class RccServer implements ModInitializer { currentPlayerCount = server.getCurrentPlayerCount() - 1; }); + PlayerActivityEvents.AFK.register((player, server) -> { + LOGGER.info("{} is AFK. Active time: {} seconds.", player, afkTracker.getActiveTime(player)); + }); + + PlayerActivityEvents.AFK_RETURN.register((player, server) -> { + LOGGER.info("{} is no longer AFK. Active time: {} seconds.", player, afkTracker.getActiveTime(player)); + }); } public void broadcastMessage(MinecraftServer server, Text message) { @@ -131,4 +140,16 @@ public class RccServer implements ModInitializer { player.sendMessage(message, false); } } + + public boolean isPlayerAfk(PlayerEntity player) { + return afkTracker.isPlayerAfk(player.getUuid()); + } + + public void setPlayerAfk(ServerPlayerEntity player, boolean afk) { + afkTracker.setPlayerAfk(player, afk); + } + + public int getActiveTime(ServerPlayerEntity player) { + return afkTracker().getActiveTime(player); + } } \ No newline at end of file diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index 27e9572..37743ca 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -4,5 +4,8 @@ import io.wispforest.owo.config.annotation.Config; @Config(name = "rcc-server-config", wrapperName = "RccServerConfig") public class RccServerConfigModel { - public short httpPort = 25581; + public boolean enableHttpApi = true; + public int httpPort = 25581; + + public int afkTimeTrigger = 300; } diff --git a/src/main/java/cc/reconnected/server/commands/AfkCommand.java b/src/main/java/cc/reconnected/server/commands/AfkCommand.java new file mode 100644 index 0000000..556f417 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/AfkCommand.java @@ -0,0 +1,32 @@ +package cc.reconnected.server.commands; + +import cc.reconnected.server.RccServer; +import com.mojang.brigadier.CommandDispatcher; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +import static net.minecraft.server.command.CommandManager.literal; + +public class AfkCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("afk") + .requires(Permissions.require("rcc.afk.command", true)) + .executes(context -> { + + if(!context.getSource().isExecutedByPlayer()) { + context.getSource().sendFeedback(() -> Text.of("This command can only be executed by players!"), false); + return 1; + } + + var player = context.getSource().getPlayer(); + RccServer.getInstance().setPlayerAfk(player, true); + + return 1; + }); + + dispatcher.register(rootCommand); + } +} diff --git a/src/main/java/cc/reconnected/server/commands/RccCommand.java b/src/main/java/cc/reconnected/server/commands/RccCommand.java index 1ad5117..be0d1be 100644 --- a/src/main/java/cc/reconnected/server/commands/RccCommand.java +++ b/src/main/java/cc/reconnected/server/commands/RccCommand.java @@ -1,11 +1,9 @@ package cc.reconnected.server.commands; -import cc.reconnected.server.RccServer; import com.mojang.brigadier.CommandDispatcher; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.text.Text; import static net.minecraft.server.command.CommandManager.literal; diff --git a/src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java b/src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java new file mode 100644 index 0000000..204eebc --- /dev/null +++ b/src/main/java/cc/reconnected/server/data/StateSaverAndLoader.java @@ -0,0 +1,59 @@ +package cc.reconnected.server.data; + +import cc.reconnected.server.RccServer; +import net.minecraft.entity.LivingEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.PersistentState; +import net.minecraft.world.World; + +import java.util.HashMap; +import java.util.UUID; + +public class StateSaverAndLoader extends PersistentState { + public final HashMap players = new HashMap<>(); + + @Override + public NbtCompound writeNbt(NbtCompound nbt) { + var playersNbt = new NbtCompound(); + players.forEach((uuid, data) -> { + var playerNbt = new NbtCompound(); + playerNbt.putInt("activeTime", data.activeTime); + playersNbt.put(uuid.toString(), playerNbt); + }); + nbt.put("players", playersNbt); + + return nbt; + } + + public static StateSaverAndLoader createFromNbt(NbtCompound nbt) { + var state = new StateSaverAndLoader(); + + var playersNbt = nbt.getCompound("players"); + playersNbt.getKeys().forEach(key -> { + var playerData = new WorldPlayerData(); + + playerData.activeTime = playersNbt.getCompound(key).getInt("activeTime"); + UUID uuid = UUID.fromString(key); + state.players.put(uuid, playerData); + }); + + return state; + } + + public static StateSaverAndLoader getServerState(MinecraftServer server) { + var persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); + var state = persistentStateManager.getOrCreate( + StateSaverAndLoader::createFromNbt, + StateSaverAndLoader::new, + RccServer.MOD_ID + ); + state.markDirty(); + return state; + } + + public static WorldPlayerData getPlayerState(LivingEntity player) { + var serverState = getServerState(player.getWorld().getServer()); + return serverState.players.computeIfAbsent(player.getUuid(), uuid -> new WorldPlayerData()); + } +} diff --git a/src/main/java/cc/reconnected/server/data/WorldPlayerData.java b/src/main/java/cc/reconnected/server/data/WorldPlayerData.java new file mode 100644 index 0000000..fcec6f1 --- /dev/null +++ b/src/main/java/cc/reconnected/server/data/WorldPlayerData.java @@ -0,0 +1,5 @@ +package cc.reconnected.server.data; + +public class WorldPlayerData { + public int activeTime = 0; +} diff --git a/src/main/java/cc/reconnected/server/database/PlayerData.java b/src/main/java/cc/reconnected/server/database/PlayerData.java index 070886f..bdb0b52 100644 --- a/src/main/java/cc/reconnected/server/database/PlayerData.java +++ b/src/main/java/cc/reconnected/server/database/PlayerData.java @@ -31,6 +31,7 @@ public class PlayerData { public static final String pronouns = "pronouns"; public static final String firstJoinedDate = "first_joined_date"; public static final String supporterLevel = "supporter_level"; + public static final String activeTime = "active_time"; } private final User lpUser; diff --git a/src/main/java/cc/reconnected/server/trackers/AfkTracker.java b/src/main/java/cc/reconnected/server/trackers/AfkTracker.java index b62bdd4..494b87e 100644 --- a/src/main/java/cc/reconnected/server/trackers/AfkTracker.java +++ b/src/main/java/cc/reconnected/server/trackers/AfkTracker.java @@ -1,12 +1,13 @@ package cc.reconnected.server.trackers; import cc.reconnected.server.RccServer; +import cc.reconnected.server.data.StateSaverAndLoader; +import cc.reconnected.server.database.PlayerData; 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; @@ -16,11 +17,10 @@ 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<>(); + private static final int cycleDelay = 1; + private static final int absentTimeTrigger = RccServer.CONFIG.afkTimeTrigger() * 20; // seconds * 20 ticks + + private final HashMap playerStates = new HashMap<>(); public AfkTracker() { ServerTickEvents.END_SERVER_TICK.register(server -> { @@ -31,16 +31,18 @@ public class AfkTracker { 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); + playerStates.put(player.getUuid(), new PlayerState(player, server.getTicks())); }); ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { - playerPositions.remove(handler.getPlayer().getUuid()); - playerLastUpdate.remove(handler.getPlayer().getUuid()); - playerAfkStates.remove(handler.getPlayer().getUuid()); + updatePlayerActiveTime(handler.getPlayer(), server.getTicks()); + playerStates.remove(handler.getPlayer().getUuid()); + + // sync to LP + var activeTime = String.valueOf(getActiveTime(handler.getPlayer())); + var playerData = PlayerData.getPlayer(handler.getPlayer()); + + playerData.set(PlayerData.KEYS.activeTime, activeTime).join(); }); AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> { @@ -74,46 +76,60 @@ public class AfkTracker { }); ServerMessageEvents.ALLOW_COMMAND_MESSAGE.register((message, source, params) -> { - if(!source.isExecutedByPlayer()) + if (!source.isExecutedByPlayer()) return true; resetAfkState(source.getPlayer(), source.getServer()); return true; }); } - public void updatePlayers(MinecraftServer server) { - var players = server.getPlayerManager().getPlayerList(); + + private void updatePlayer(ServerPlayerEntity player, MinecraftServer server) { var currentTick = server.getTicks(); + var playerState = playerStates.computeIfAbsent(player.getUuid(), uuid -> new PlayerState(player, currentTick)); + + var oldPosition = playerState.position; + var newPosition = new PlayerPosition(player); + if (!oldPosition.equals(newPosition)) { + playerState.position = newPosition; + resetAfkState(player, server); + return; + } + + if (playerState.isAfk) + return; + + if ((playerState.lastUpdate + absentTimeTrigger) <= currentTick) { + // player is afk after 5 mins + updatePlayerActiveTime(player, currentTick); + playerState.isAfk = true; + PlayerActivityEvents.AFK.invoker().onAfk(player, server); + } + } + + private void updatePlayerActiveTime(ServerPlayerEntity player, int currentTick) { + var playerState = playerStates.get(player.getUuid()); + if(!playerState.isAfk) { + var worldPlayerData = StateSaverAndLoader.getPlayerState(player); + var interval = currentTick - playerState.activeStart; + worldPlayerData.activeTime += interval / 20; + } + } + + private void updatePlayers(MinecraftServer server) { + var players = server.getPlayerManager().getPlayerList(); 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); - } + updatePlayer(player, server); }); } private void resetAfkState(ServerPlayerEntity player, MinecraftServer server) { - playerLastUpdate.put(player.getUuid(), server.getTicks()); - if (playerAfkStates.get(player.getUuid())) { + var playerState = playerStates.get(player.getUuid()); + playerState.lastUpdate = server.getTicks(); + if (playerState.isAfk) { + playerState.isAfk = false; + playerState.activeStart = server.getTicks(); PlayerActivityEvents.AFK_RETURN.invoker().onAfkReturn(player, server); - playerAfkStates.put(player.getUuid(), false); } } @@ -141,4 +157,47 @@ public class AfkTracker { pitch = player.getPitch(); } } + + public static class PlayerState { + public PlayerPosition position; + public int lastUpdate; + public boolean isAfk; + public int activeStart; + + public PlayerState(ServerPlayerEntity player, int lastUpdate) { + this.position = new PlayerPosition(player); + this.lastUpdate = lastUpdate; + this.isAfk = false; + this.activeStart = lastUpdate; + } + } + + public boolean isPlayerAfk(UUID playerUuid) { + if (!playerStates.containsKey(playerUuid)) { + return false; + } + return playerStates.get(playerUuid).isAfk; + } + + public void setPlayerAfk(ServerPlayerEntity player, boolean afk) { + if (!playerStates.containsKey(player.getUuid())) { + return; + } + + var server = player.getWorld().getServer(); + + if (afk) { + playerStates.get(player.getUuid()).lastUpdate = -absentTimeTrigger - 20; // just to be sure + } else { + resetAfkState(player, server); + } + + updatePlayer(player, server); + } + + public int getActiveTime(ServerPlayerEntity player) { + var worldPlayerData = StateSaverAndLoader.getPlayerState(player); + return worldPlayerData.activeTime; + } + }