Add AFK and active time support
This commit is contained in:
parent
1d353f89f5
commit
d07fade5ab
10 changed files with 227 additions and 47 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
32
src/main/java/cc/reconnected/server/commands/AfkCommand.java
Normal file
32
src/main/java/cc/reconnected/server/commands/AfkCommand.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package cc.reconnected.server.data;
|
||||
|
||||
public class WorldPlayerData {
|
||||
public int activeTime = 0;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue