Add AFK and active time support

This commit is contained in:
Alessandro Proto 2024-10-13 19:24:25 +02:00
parent 1d353f89f5
commit d07fade5ab
10 changed files with 227 additions and 47 deletions

View file

@ -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 {

View file

@ -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
@ -19,3 +19,4 @@ fabric_version=0.92.2+1.20.1
owo_version=0.11.2+1.20
luckpermsapi_version=5.4
permissions_api_version=0.2-SNAPSHOT

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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<ServerCommandSource> 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);
}
}

View file

@ -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;

View file

@ -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<UUID, WorldPlayerData> 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());
}
}

View file

@ -0,0 +1,5 @@
package cc.reconnected.server.data;
public class WorldPlayerData {
public int activeTime = 0;
}

View file

@ -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;

View file

@ -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<UUID, PlayerPosition> playerPositions = new HashMap<>();
private final HashMap<UUID, Integer> playerLastUpdate = new HashMap<>();
private final HashMap<UUID, Boolean> playerAfkStates = new HashMap<>();
private static final int cycleDelay = 1;
private static final int absentTimeTrigger = RccServer.CONFIG.afkTimeTrigger() * 20; // seconds * 20 ticks
private final HashMap<UUID, PlayerState> 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;
}
}