AFKChecker

  • community
  • anti-cheat
  • by limon
  • Takaro main
  • all
Version
View export JSON

checks for players if they are afk and kicks them

Configuration 6

Settings you fill in when installing the module on a server.

SettingTypeDefaultDescription
minutesBeforeKick required Minutes Between Checks number 5 Time in minutes between each AFK check. This should match your cronjob schedule (for example, if your cronjob runs every 5 minutes, set this to 5). Total AFK time before kick will be (Minutes Between Checks × Maximum AFK Checks).
maxAfkChecks required Maximum AFK Checks number 3 Number of consecutive AFK checks before a player is kicked. Total AFK time will be (Minutes Between Checks × Maximum AFK Checks). For example, with 5 minutes between checks and 3 max checks, players will be kicked after 15 minutes of being AFK.
kickMessage required Kick Message string "You have been kicked for being AFK for {minutesAfk} minutes" Message shown to the player when they are kicked for being AFK. You can use {minutesAfk} and {minutesUntilKick} placeholders.
sendWarning Send Warning boolean true Whether to send a warning to players before kicking them for being AFK
warningMessage required Warning Message string "Warning: You have been AFK for {minutesAfk} minutes. You will be kicked in {minutesUntilKick} minutes unless you move!" Message sent to warn players before they are kicked for being AFK. You can use {minutesAfk} and {minutesUntilKick} placeholders.
globalAnnouncement Global Announcement boolean false Whether to announce AFK warnings and kicks to all players on the server
Raw config schema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": [
    "maxAfkChecks",
    "minutesBeforeKick",
    "kickMessage",
    "warningMessage"
  ],
  "additionalProperties": false,
  "properties": {
    "minutesBeforeKick": {
      "title": "Minutes Between Checks",
      "description": "Time in minutes between each AFK check. This should match your cronjob schedule (for example, if your cronjob runs every 5 minutes, set this to 5). Total AFK time before kick will be (Minutes Between Checks × Maximum AFK Checks).",
      "type": "number",
      "default": 5,
      "minimum": 1
    },
    "maxAfkChecks": {
      "title": "Maximum AFK Checks",
      "description": "Number of consecutive AFK checks before a player is kicked. Total AFK time will be (Minutes Between Checks × Maximum AFK Checks). For example, with 5 minutes between checks and  3 max checks, players will be kicked after 15 minutes of being AFK.",
      "default": 3,
      "type": "number",
      "minimum": 1
    },
    "kickMessage": {
      "title": "Kick Message",
      "description": "Message shown to the player when they are kicked for being AFK. You can use {minutesAfk} and {minutesUntilKick} placeholders.",
      "default": "You have been kicked for being AFK for {minutesAfk} minutes",
      "type": "string"
    },
    "sendWarning": {
      "title": "Send Warning",
      "description": "Whether to send a warning to players before kicking them for being AFK",
      "default": true,
      "type": "boolean"
    },
    "warningMessage": {
      "title": "Warning Message",
      "description": "Message sent to warn players before they are kicked for being AFK. You can use {minutesAfk} and {minutesUntilKick} placeholders.",
      "default": "Warning: You have been AFK for {minutesAfk} minutes. You will be kicked in {minutesUntilKick} minutes unless you move!",
      "type": "string"
    },
    "globalAnnouncement": {
      "title": "Global Announcement",
      "description": "Whether to announce AFK warnings and kicks to all players on the server",
      "default": false,
      "type": "boolean"
    }
  }
}
Raw UI schema
{}

Cron jobs 1

Work the module runs on a schedule.

  • afkChecker

    Cron job source
    import { takaro, data, checkPermission } from '@takaro/helpers';
    
    async function main() {
        const { gameServerId, module: mod } = data;
    
        // Get configuration values directly from userConfig
        const maxAfkChecks = mod.userConfig.maxAfkChecks;
        const kickMessage = mod.userConfig.kickMessage;
        const sendWarning = mod.userConfig.sendWarning;
        const warningMessage = mod.userConfig.warningMessage;
        const globalAnnouncement = mod.userConfig.globalAnnouncement;
        const minutesBetweenChecks = mod.userConfig.minutesBeforeKick;
    
        // Add a small position tolerance to avoid false AFK positives
        const positionTolerance = 1.0; // Allow 1 unit of movement without resetting AFK
    
        // First, let's clean up variables for offline players
        await cleanupOfflinePlayerVariables(gameServerId, mod.moduleId);
    
        // Get online players through PlayerOnGameServer search
        const playersResponse = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
            filters: {
                gameServerId: [gameServerId],
                online: [true]
            }
        });
    
        // If no players online, exit early
        if (playersResponse.data.meta.total === 0) {
            return;
        }
    
        // Process each online player
        for (const playerData of playersResponse.data.data) {
            // Get the player's POG data to check permissions
            const pogResponse = await takaro.playerOnGameserver.playerOnGameServerControllerGetOne(gameServerId, playerData.player.id);
            const pog = pogResponse.data.data;
    
            // Check if player has immunity
            let hasImmunity = false;
            try {
                // Checking if the player has the permission
                if (pog) {
                    hasImmunity = checkPermission(pog, 'AFK_IMMUNITY');
                }
            } catch (e) {
                // If there's an error checking permissions, assume no immunity
                hasImmunity = false;
            }
    
            // Skip players with AFK immunity
            if (hasImmunity) {
                continue;
            }
    
            // Look for the player's last position
            const lastPositionVar = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['afk_last_position'],
                    playerId: [playerData.player.id],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId]
                }
            });
    
            // Get current position from playerData
            const currentPosition = {
                x: playerData.positionX,
                y: playerData.positionY,
                z: playerData.positionZ
            };
    
            if (lastPositionVar.data.data.length === 0) {
                // First time seeing this player, store their position
                await takaro.variable.variableControllerCreate({
                    key: 'afk_last_position',
                    value: JSON.stringify(currentPosition),
                    playerId: playerData.player.id,
                    gameServerId: gameServerId,
                    moduleId: mod.moduleId
                });
    
                // Also initialize their AFK counter
                await takaro.variable.variableControllerCreate({
                    key: 'afk_check_count',
                    value: '0',
                    playerId: playerData.player.id,
                    gameServerId: gameServerId,
                    moduleId: mod.moduleId
                });
                continue;
            }
    
            // Parse the last saved position
            const lastPosition = JSON.parse(lastPositionVar.data.data[0].value);
    
            // Get the AFK counter
            const afkCountVar = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['afk_check_count'],
                    playerId: [playerData.player.id],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId]
                }
            });
    
            let afkCount = 0;
            if (afkCountVar.data.data.length > 0) {
                afkCount = parseInt(afkCountVar.data.data[0].value);
            }
    
            // Calculate distance moved
            const distanceX = Math.abs(currentPosition.x - lastPosition.x);
            const distanceY = Math.abs(currentPosition.y - lastPosition.y);
            const distanceZ = Math.abs(currentPosition.z - lastPosition.z);
    
            // Consider player moved if they moved more than the tolerance in any direction
            const hasPlayerMoved = (
                distanceX > positionTolerance ||
                distanceY > positionTolerance ||
                distanceZ > positionTolerance
            );
    
            if (hasPlayerMoved) {
                // Player has moved, reset AFK counter
                if (afkCountVar.data.data.length > 0) {
                    await takaro.variable.variableControllerUpdate(afkCountVar.data.data[0].id, {
                        value: '0'
                    });
                }
    
                // Update last position
                await takaro.variable.variableControllerUpdate(lastPositionVar.data.data[0].id, {
                    value: JSON.stringify(currentPosition)
                });
            } else {
                // Player hasn't moved, increment AFK counter
                afkCount++;
    
                await takaro.variable.variableControllerUpdate(afkCountVar.data.data[0].id, {
                    value: afkCount.toString()
                });
    
                // Calculate dynamic message values
                const checksLeft = maxAfkChecks - afkCount;
                const minutesAfk = afkCount * minutesBetweenChecks;
                const minutesUntilKick = checksLeft * minutesBetweenChecks;
    
                // Create dynamic messages with placeholders replaced
                const personalWarningMsg = warningMessage
                    .replace('{warningsLeft}', checksLeft)
                    .replace('{minutesAfk}', minutesAfk)
                    .replace('{minutesUntilKick}', minutesUntilKick);
    
                const personalKickMsg = kickMessage
                    .replace('{warningsLeft}', checksLeft)
                    .replace('{minutesAfk}', minutesAfk)
                    .replace('{minutesUntilKick}', minutesUntilKick);
    
                // Send warning if configured and there are checks left before max
                if (sendWarning && checksLeft > 0) {
                    // Send personal warning message to the player
                    await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                        message: personalWarningMsg,
                        opts: {
                            recipient: {
                                gameId: pog.gameId,
                            }
                        }
                    });
    
                    // Send global announcement if configured
                    if (globalAnnouncement) {
                        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                            message: `${playerData.player.name} has been AFK for ${minutesAfk} minutes and will be kicked in ${minutesUntilKick} minutes unless they move!`
                        });
                    }
                }
    
                // If AFK for too long, kick the player
                if (afkCount >= maxAfkChecks) {
                    // Using the execute command approach which is more reliable
                    await takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, {
                        command: `kick "${playerData.player.name}" "${personalKickMsg}"`
                    });
    
                    // Send global announcement if configured
                    if (globalAnnouncement) {
                        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                            message: `${playerData.player.name} has been kicked for being AFK for ${minutesAfk} minutes.`
                        });
                    }
    
                    // Reset counter after kicking
                    await takaro.variable.variableControllerUpdate(afkCountVar.data.data[0].id, {
                        value: '0'
                    });
                }
    
                // Update last position even if they haven't moved significantly
                await takaro.variable.variableControllerUpdate(lastPositionVar.data.data[0].id, {
                    value: JSON.stringify(currentPosition)
                });
            }
        }
    }
    
    // Helper function to clean up variables for offline players
    async function cleanupOfflinePlayerVariables(gameServerId, moduleId) {
        // Get all variables for this module and gameserver
        const allVariables = await takaro.variable.variableControllerSearch({
            filters: {
                gameServerId: [gameServerId],
                moduleId: [moduleId]
            }
        });
    
        if (allVariables.data.data.length === 0) {
            return;
        }
    
        // Group variables by player ID
        const variablesByPlayer = {};
        for (const variable of allVariables.data.data) {
            if (variable.playerId) {
                if (!variablesByPlayer[variable.playerId]) {
                    variablesByPlayer[variable.playerId] = [];
                }
                variablesByPlayer[variable.playerId].push(variable);
            }
        }
    
        // Get list of online player IDs for fast lookup
        const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
            filters: {
                gameServerId: [gameServerId],
                online: [true]
            }
        });
    
        const onlinePlayerIds = new Set(onlinePlayers.data.data.map(pog => pog.player.id));
    
        // Delete variables for offline players
        for (const playerId in variablesByPlayer) {
            if (!onlinePlayerIds.has(playerId)) {
                for (const variable of variablesByPlayer[playerId]) {
                    await takaro.variable.variableControllerDelete(variable.id);
                }
            }
        }
    }
    
    await main();

Permissions 1

Roles you can grant to decide who may use what.

  • AFK Kick Immunity

    Players with this permission will not be kicked for being AFK.