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