Page 1 of 1

A little helper for the Admins

Posted: 08 Apr 2026, 19:33
by _eXecute_
Hello all i just got a little bored ands thought i tried to do some lua scripting again after years and i hope no one is offended when i say the current antirush seems at moment a bit outdated. I understand we are all busy on different stuff so this is not a critic i just used this as prototype how i would approach it.

This is a little prototype with a ingame easy setup. I still need to test it further to make it bulletproof i just thought before i invest more time in this if that would make sense.

I hope this is displayed correct.

Code: Select all

-- AutoAdmin
-- ET 2.60b / N!tmod
--
-- Commands:
--   aa help
--   aa antirush
--   aa antirush [ID]
--   aa trickplant
--   aa trickplant [ID]
--   aa list
--   aa remove [NUMBER]
--   aa edit [NUMBER] [DISTANCE]
--
-- Authorization:
--   Commands only work for client slot 0
--   and only if the player's n_guid is listed in:
--   luas/autoadmin/guid.cfg
--
-- File format:
--   luas/autoadmin/protected_maps/[mapname].cfg
--
--   [antirush]
--   "Shortname" X Y Z Distance
--
--   [trickplant]
--   X Y Z Distance
--
-- Behavior:
--   Antirush:
--     During the first 30% of the map timelimit, rushing protected objectives
--     is blocked. Player is killed, planted dynamite is removed.
--
--   Trickplant:
--     If a player plants dynamite inside a protected trickplant area,
--     the planted dynamite is removed, but the player is not killed.

local MODNAME = "AutoAdmin"
local VERSION = "2.0"

local MAX_GENTITIES = 1024
local ANTIRUSH_SCAN_RANGE = 1000
local TRICKPLANT_SCAN_RANGE = 650
local PROTECT_PERCENT = 0.30

local GUID_FILE = "luas/autoadmin/guid.cfg"
local MAP_DIR = "luas/autoadmin/protected_maps/"

local antirushEntries = {}
local trickplantEntries = {}

local currentLevelTime = 0
local protectionStartLevelTime = 0
local announceDone = false
local announceAtLevelTime = 0
local protectionExpiredAnnounced = false

local DYNAMITE_PLANT_PATTERN = '^Dynamite_Plant:%s*(%d+)%s*$'

local function printToClient(clientNum, text)
    et.trap_SendServerCommand(clientNum, 'print "' .. text .. '\n"')
end

local function chatAll(text)
    et.trap_SendServerCommand(-1, 'chat "' .. text .. '\n"')
end

local function appendLine(fd, line)
    local text = tostring(line) .. "\n"
    et.trap_FS_Write(text, string.len(text), fd)
end

local function trim(s)
    if not s then
        return ""
    end
    return string.gsub(string.gsub(s, "^%s+", ""), "%s+$", "")
end

local function sanitizeMapname(name)
    if not name or name == "" then
        return "unknownmap"
    end
    return string.gsub(name, "[^%w_%-%+]", "_")
end

local function sanitizeText(value)
    if not value or value == "" then
        return "-"
    end

    value = tostring(value)
    value = string.gsub(value, '"', "'")
    value = string.gsub(value, "[\r\n]", " ")
    return value
end

local function tryGetField(entNum, fieldName)
    local ok, value = pcall(et.gentity_get, entNum, fieldName)
    if ok and value ~= nil then
        return value
    end
    return nil
end

local function vectorToXYZ(v)
    if type(v) ~= "table" then
        return nil, nil, nil
    end

    if v[1] ~= nil and v[2] ~= nil and v[3] ~= nil then
        return tonumber(v[1]), tonumber(v[2]), tonumber(v[3])
    end

    return nil, nil, nil
end

local function parseOriginString(origin)
    if not origin or origin == "" then
        return nil, nil, nil
    end

    local x, y, z = string.match(
        tostring(origin),
        "([%-%.%d]+)%s+([%-%.%d]+)%s+([%-%.%d]+)"
    )

    if x and y and z then
        return tonumber(x), tonumber(y), tonumber(z)
    end

    return nil, nil, nil
end

local function distance3d(x1, y1, z1, x2, y2, z2)
    local dx = x2 - x1
    local dy = y2 - y1
    local dz = z2 - z1
    return math.sqrt(dx * dx + dy * dy + dz * dz)
end

local function getSpawnVar(entNum, key)
    local ok, value = pcall(et.G_GetSpawnVar, entNum, key)
    if ok and value and value ~= "" then
        return tostring(value)
    end
    return nil
end

local function getMapName()
    local mapname = et.trap_Cvar_Get("mapname")
    if not mapname or mapname == "" then
        mapname = "unknownmap"
    end
    return sanitizeMapname(mapname)
end

local function getMapFilePath()
    return MAP_DIR .. getMapName() .. ".cfg"
end

local function getMapTimeLimitMinutes()
    local timelimit = et.trap_Cvar_Get("timelimit")
    local n = tonumber(timelimit)
    if not n or n < 0 then
        return 0
    end
    return n
end

local function getProtectMinutes()
    return getMapTimeLimitMinutes() * PROTECT_PERCENT
end

local function getProtectMilliseconds()
    return getProtectMinutes() * 60000.0
end

local function isProtectionActive()
    return currentLevelTime < (protectionStartLevelTime + getProtectMilliseconds())
end

local function readWholeFile(path)
    local fd, len = et.trap_FS_FOpenFile(path, et.FS_READ)
    if not fd or fd < 0 then
        return nil
    end

    if not len or len <= 0 then
        et.trap_FS_FCloseFile(fd)
        return ""
    end

    local content = et.trap_FS_Read(fd, len)
    et.trap_FS_FCloseFile(fd)
    return content
end

local function getPlayerOrigin(clientNum)
    local vec = tryGetField(clientNum, "ps.origin")
    if vec then
        local x, y, z = vectorToXYZ(vec)
        if x and y and z then
            return x, y, z
        end
    end

    vec = tryGetField(clientNum, "r.currentOrigin")
    if vec then
        local x, y, z = vectorToXYZ(vec)
        if x and y and z then
            return x, y, z
        end
    end

    return nil, nil, nil
end

local function getPlayerName(clientNum)
    local userinfo = et.trap_GetUserinfo(clientNum)
    if not userinfo or userinfo == "" then
        return tostring(clientNum)
    end

    local name = et.Info_ValueForKey(userinfo, "name")
    if not name or name == "" then
        return tostring(clientNum)
    end

    return name
end

local function getPlayerNGuid(clientNum)
    local userinfo = et.trap_GetUserinfo(clientNum)
    if not userinfo or userinfo == "" then
        return nil
    end

    local nguid = et.Info_ValueForKey(userinfo, "n_guid")
    if not nguid or nguid == "" then
        return nil
    end

    return nguid
end

local allowedGuids = {}

local function loadAllowedGuids()
    allowedGuids = {}

    local content = readWholeFile(GUID_FILE)
    if not content then
        et.G_Print(MODNAME .. ": no guid file found at " .. GUID_FILE .. "\n")
        return
    end

    for line in string.gmatch(content, "[^\r\n]+") do
        local guid = trim(line)
        if guid ~= "" then
            allowedGuids[guid] = true
        end
    end

    et.G_Print(MODNAME .. ": loaded allowed GUIDs from " .. GUID_FILE .. "\n")
end

local function isAuthorized(clientNum)
    if clientNum ~= 0 then
        return false
    end

    local nguid = getPlayerNGuid(clientNum)
    if not nguid then
        return false
    end

    return allowedGuids[nguid] == true
end

local function requireAuthorization(clientNum)
    if isAuthorized(clientNum) then
        return true
    end

    printToClient(clientNum, "^1AUTOADMIN:^7 command not authorized")
    return false
end

local function getEntityClassname(entNum)
    local classname = tryGetField(entNum, "classname")
    if classname then
        return tostring(classname)
    end

    classname = getSpawnVar(entNum, "classname")
    if classname then
        return classname
    end

    return nil
end

local function isAntirushClass(classname)
    return classname == "team_WOLF_objective"
        or classname == "team_CTF_redflag"
        or classname == "team_CTF_blueflag"
        or classname == "trigger_objective_info"
end

local function isDynamiteClass(classname)
    if not classname then
        return false
    end
    return string.find(string.lower(classname), "dynamite", 1, true) ~= nil
end

local function getEntityShortname(entNum)
    local shortname = getSpawnVar(entNum, "shortname")
    if shortname and shortname ~= "" then
        return shortname
    end

    local message = getSpawnVar(entNum, "message")
    if message and message ~= "" then
        return message
    end

    local targetname = getSpawnVar(entNum, "targetname")
    if targetname and targetname ~= "" then
        return targetname
    end

    return "unknown"
end

local function getEntityOrigin(entNum)
    local origin = getSpawnVar(entNum, "origin")
    if origin then
        local x, y, z = parseOriginString(origin)
        if x and y and z then
            return x, y, z
        end
    end

    local vec = tryGetField(entNum, "r.currentOrigin")
    if vec then
        local x, y, z = vectorToXYZ(vec)
        if x and y and z then
            return x, y, z
        end
    end

    return nil, nil, nil
end

local function isWithinBoxRange(px, py, pz, ex, ey, ez, range)
    return math.abs(ex - px) <= range
       and math.abs(ey - py) <= range
       and math.abs(ez - pz) <= range
end

local function writeProtectedFile()
    local path = getMapFilePath()
    local fd, len = et.trap_FS_FOpenFile(path, et.FS_WRITE)

    if not fd or fd < 0 then
        return false, path
    end

    appendLine(fd, "[antirush]")
    for i = 1, #antirushEntries do
        local e = antirushEntries[i]
        appendLine(fd, string.format('"%s" %.3f %.3f %.3f %.3f',
            sanitizeText(e.shortname), e.x, e.y, e.z, e.distance))
    end

    appendLine(fd, "")
    appendLine(fd, "[trickplant]")
    for i = 1, #trickplantEntries do
        local e = trickplantEntries[i]
        appendLine(fd, string.format('%.3f %.3f %.3f %.3f',
            e.x, e.y, e.z, e.distance))
    end

    et.trap_FS_FCloseFile(fd)
    return true, path
end

local function loadProtectedFile()
    antirushEntries = {}
    trickplantEntries = {}

    local content = readWholeFile(getMapFilePath())
    if not content then
        et.G_Print(MODNAME .. ": no protected map cfg found for " .. getMapName() .. "\n")
        return
    end

    local section = ""

    for line in string.gmatch(content, "[^\r\n]+") do
        line = trim(line)

        if line ~= "" then
            if line == "[antirush]" then
                section = "antirush"
            elseif line == "[trickplant]" then
                section = "trickplant"
            elseif section == "antirush" then
                local shortname, x, y, z, distance = string.match(
                    line,
                    '^"(.-)"%s+([%-%.%d]+)%s+([%-%.%d]+)%s+([%-%.%d]+)%s+([%-%.%d]+)%s*$'
                )

                if shortname and x and y and z and distance then
                    antirushEntries[#antirushEntries + 1] = {
                        shortname = shortname,
                        x = tonumber(x),
                        y = tonumber(y),
                        z = tonumber(z),
                        distance = tonumber(distance)
                    }
                end
            elseif section == "trickplant" then
                local x, y, z, distance = string.match(
                    line,
                    '^([%-%.%d]+)%s+([%-%.%d]+)%s+([%-%.%d]+)%s+([%-%.%d]+)%s*$'
                )

                if x and y and z and distance then
                    trickplantEntries[#trickplantEntries + 1] = {
                        x = tonumber(x),
                        y = tonumber(y),
                        z = tonumber(z),
                        distance = tonumber(distance)
                    }
                end
            end
        end
    end

    et.G_Print(MODNAME .. ": loaded " .. tostring(#antirushEntries) .. " antirush entries and "
        .. tostring(#trickplantEntries) .. " trickplant entries\n")
end

local function getAntirushNamesString()
    local names = {}
    for i = 1, #antirushEntries do
        names[#names + 1] = antirushEntries[i].shortname
    end
    return table.concat(names, ", ")
end

local function announceProtectedObjectives()
    if #antirushEntries == 0 then
        return
    end

    chatAll(string.format(
        '^3AUTOADMIN:^7 Rushing the following objectives is prohibited for the first ^3%.1f ^7minutes: ^3%s',
        getProtectMinutes(),
        getAntirushNamesString()
    ))
end

local function announceProtectionEnded()
    if #antirushEntries == 0 then
        return
    end

    chatAll(string.format(
        '^3AUTOADMIN:^7 The following objectives are no longer protected: ^3%s',
        getAntirushNamesString()
    ))
end

local function buildCombinedList()
    local combined = {}

    for i = 1, #antirushEntries do
        combined[#combined + 1] = {
            type = "antirush",
            index = i
        }
    end

    for i = 1, #trickplantEntries do
        combined[#combined + 1] = {
            type = "trickplant",
            index = i
        }
    end

    return combined
end

local function listSavedEntries(clientNum)
    local combined = buildCombinedList()

    printToClient(clientNum, "^3AUTOADMIN")

    if #combined == 0 then
        printToClient(clientNum, "^1No saved entries.")
        return
    end

    for i = 1, #combined do
        local ref = combined[i]
        if ref.type == "antirush" then
            local e = antirushEntries[ref.index]
            printToClient(clientNum,
                string.format("%d - ANTIRUSH - %s - %.0f", i, sanitizeText(e.shortname), e.distance))
        else
            local e = trickplantEntries[ref.index]
            printToClient(clientNum,
                string.format("%d - TRICKPLANT - %.3f %.3f %.3f - %.0f", i, e.x, e.y, e.z, e.distance))
        end
    end

    printToClient(clientNum, "^3aa remove [NUMBER]^7 removes an entry.")
    printToClient(clientNum, "^3aa edit [NUMBER] [DISTANCE]^7 changes the distance.")
end

local function removeSavedEntry(clientNum, wantedIndex)
    local idx = tonumber(wantedIndex)
    if not idx then
        printToClient(clientNum, "^1aa remove:^7 invalid number")
        return
    end

    local combined = buildCombinedList()
    if idx < 1 or idx > #combined then
        printToClient(clientNum, "^1aa remove:^7 number out of range")
        return
    end

    local ref = combined[idx]
    local removedText

    if ref.type == "antirush" then
        removedText = sanitizeText(antirushEntries[ref.index].shortname)
        table.remove(antirushEntries, ref.index)
    else
        local e = trickplantEntries[ref.index]
        removedText = string.format("%.3f %.3f %.3f", e.x, e.y, e.z)
        table.remove(trickplantEntries, ref.index)
    end

    local ok, path = writeProtectedFile()
    if not ok then
        printToClient(clientNum, "^1aa remove:^7 failed to write " .. path)
        return
    end

    printToClient(clientNum, string.format('^3aa remove:^7 removed %s', removedText))
end

local function editSavedEntryDistance(clientNum, wantedIndex, wantedDistance)
    local idx = tonumber(wantedIndex)
    local distance = tonumber(wantedDistance)

    if not idx then
        printToClient(clientNum, "^1aa edit:^7 invalid number")
        return
    end

    if not distance or distance <= 0 then
        printToClient(clientNum, "^1aa edit:^7 invalid distance")
        return
    end

    local combined = buildCombinedList()
    if idx < 1 or idx > #combined then
        printToClient(clientNum, "^1aa edit:^7 number out of range")
        return
    end

    local ref = combined[idx]
    if ref.type == "antirush" then
        antirushEntries[ref.index].distance = distance
    else
        trickplantEntries[ref.index].distance = distance
    end

    local ok, path = writeProtectedFile()
    if not ok then
        printToClient(clientNum, "^1aa edit:^7 failed to write " .. path)
        return
    end

    printToClient(clientNum, string.format("^3aa edit:^7 entry %d distance changed to %.0f", idx, distance))
end

local function findAntirushEntitiesNearPlayer(clientNum)
    local px, py, pz = getPlayerOrigin(clientNum)
    if not px then
        return nil, "could not read your position"
    end

    local results = {}

    for entNum = 0, MAX_GENTITIES - 1 do
        local classname = getEntityClassname(entNum)
        if classname and isAntirushClass(classname) then
            local x, y, z = getEntityOrigin(entNum)
            if x and y and z and isWithinBoxRange(px, py, pz, x, y, z, ANTIRUSH_SCAN_RANGE) then
                results[#results + 1] = {
                    entnum = entNum,
                    shortname = sanitizeText(getEntityShortname(entNum)),
                    x = x,
                    y = y,
                    z = z,
                    distance = ANTIRUSH_SCAN_RANGE
                }
            end
        end
    end

    table.sort(results, function(a, b) return a.entnum < b.entnum end)
    return results, nil
end

local function listAntirushCandidates(clientNum)
    local results, err = findAntirushEntitiesNearPlayer(clientNum)
    if not results then
        printToClient(clientNum, "^1aa antirush:^7 " .. err)
        return
    end

    printToClient(clientNum, "^3AUTOADMIN ANTIRUSH")

    if #results == 0 then
        printToClient(clientNum, "^1No matching entities found.")
    else
        for i = 1, #results do
            printToClient(clientNum,
                string.format("ID: %d - NAME: %s", results[i].entnum, results[i].shortname))
        end
    end

    printToClient(clientNum, "^3aa antirush [ID]^7 saves the selected objective.")
end

local function saveAntirushById(clientNum, wantedId)
    local entnum = tonumber(wantedId)
    if not entnum then
        printToClient(clientNum, "^1aa antirush:^7 invalid id")
        return
    end

    local results, err = findAntirushEntitiesNearPlayer(clientNum)
    if not results then
        printToClient(clientNum, "^1aa antirush:^7 " .. err)
        return
    end

    local selected = nil
    for i = 1, #results do
        if results[i].entnum == entnum then
            selected = results[i]
            break
        end
    end

    if not selected then
        printToClient(clientNum, "^1aa antirush:^7 id not found in your nearby list")
        return
    end

    antirushEntries[#antirushEntries + 1] = {
        shortname = selected.shortname,
        x = selected.x,
        y = selected.y,
        z = selected.z,
        distance = selected.distance
    }

    local ok, path = writeProtectedFile()
    if not ok then
        printToClient(clientNum, "^1aa antirush:^7 failed to write " .. path)
        return
    end

    printToClient(clientNum, string.format(
        '^3aa antirush:^7 saved "%s" at %.3f %.3f %.3f with distance %.0f',
        selected.shortname, selected.x, selected.y, selected.z, selected.distance
    ))
end

local function findTrickplantEntitiesNearPlayer(clientNum)
    local px, py, pz = getPlayerOrigin(clientNum)
    if not px then
        return nil, "could not read your position"
    end

    local results = {}

    for entNum = 0, MAX_GENTITIES - 1 do
        local classname = getEntityClassname(entNum)
        if isDynamiteClass(classname) then
            local x, y, z = getEntityOrigin(entNum)
            if x and y and z and isWithinBoxRange(px, py, pz, x, y, z, TRICKPLANT_SCAN_RANGE) then
                results[#results + 1] = {
                    entnum = entNum,
                    x = x,
                    y = y,
                    z = z,
                    distance = TRICKPLANT_SCAN_RANGE
                }
            end
        end
    end

    table.sort(results, function(a, b) return a.entnum < b.entnum end)
    return results, nil
end

local function listTrickplantCandidates(clientNum)
    local results, err = findTrickplantEntitiesNearPlayer(clientNum)
    if not results then
        printToClient(clientNum, "^1aa trickplant:^7 " .. err)
        return
    end

    printToClient(clientNum, "^3AUTOADMIN TRICKPLANT")

    if #results == 0 then
        printToClient(clientNum, "^1No nearby dynamite entities found.")
    else
        for i = 1, #results do
            printToClient(clientNum,
                string.format("ID: %d - POS: %.3f %.3f %.3f", results[i].entnum, results[i].x, results[i].y, results[i].z))
        end
    end

    printToClient(clientNum, "^3aa trickplant [ID]^7 saves the selected trickplant area.")
end

local function saveTrickplantById(clientNum, wantedId)
    local entnum = tonumber(wantedId)
    if not entnum then
        printToClient(clientNum, "^1aa trickplant:^7 invalid id")
        return
    end

    local results, err = findTrickplantEntitiesNearPlayer(clientNum)
    if not results then
        printToClient(clientNum, "^1aa trickplant:^7 " .. err)
        return
    end

    local selected = nil
    for i = 1, #results do
        if results[i].entnum == entnum then
            selected = results[i]
            break
        end
    end

    if not selected then
        printToClient(clientNum, "^1aa trickplant:^7 id not found in your nearby list")
        return
    end

    trickplantEntries[#trickplantEntries + 1] = {
        x = selected.x,
        y = selected.y,
        z = selected.z,
        distance = selected.distance
    }

    local ok, path = writeProtectedFile()
    if not ok then
        printToClient(clientNum, "^1aa trickplant:^7 failed to write " .. path)
        return
    end

    printToClient(clientNum, string.format(
        '^3aa trickplant:^7 saved %.3f %.3f %.3f with distance %.0f',
        selected.x, selected.y, selected.z, selected.distance
    ))
end

local function killPlayer(clientNum)
    et.G_Damage(clientNum, clientNum, 1022, 400, 24, 0)
end

local function removeClosestDynamiteFromPlayer(clientNum)
    local px, py, pz = getPlayerOrigin(clientNum)
    if not px then
        return false
    end

    local bestEnt = nil
    local bestDist = nil

    for entNum = 0, MAX_GENTITIES - 1 do
        local classname = getEntityClassname(entNum)
        if isDynamiteClass(classname) then
            local ex, ey, ez = getEntityOrigin(entNum)
            if ex and ey and ez then
                local dist = distance3d(px, py, pz, ex, ey, ez)
                if not bestDist or dist < bestDist then
                    bestDist = dist
                    bestEnt = entNum
                end
            end
        end
    end

    if bestEnt then
        et.G_FreeEntity(bestEnt)
        et.G_Print(string.format(
            "%s: removed dynamite entity %d from client %d\n",
            MODNAME, bestEnt, clientNum
        ))
        return true
    end

    return false
end

local function findMatchingAntirushEntry(clientNum)
    local px, py, pz = getPlayerOrigin(clientNum)
    if not px then
        return nil
    end

    for i = 1, #antirushEntries do
        local e = antirushEntries[i]
        if isWithinBoxRange(px, py, pz, e.x, e.y, e.z, e.distance) then
            return e
        end
    end

    return nil
end

local function findMatchingTrickplantEntry(clientNum)
    local px, py, pz = getPlayerOrigin(clientNum)
    if not px then
        return nil
    end

    for i = 1, #trickplantEntries do
        local e = trickplantEntries[i]
        if isWithinBoxRange(px, py, pz, e.x, e.y, e.z, e.distance) then
            return e
        end
    end

    return nil
end

local function handleDynamitePlant(clientNum)
    if #antirushEntries > 0 and isProtectionActive() then
        local antirush = findMatchingAntirushEntry(clientNum)
        if antirush then
            chatAll(string.format(
                '^3AUTOADMIN:^7 %s ^7was killed for rushing protected objective ^3%s^7.',
                getPlayerName(clientNum),
                antirush.shortname
            ))
            killPlayer(clientNum)
            removeClosestDynamiteFromPlayer(clientNum)
            return
        end
    end

    if #trickplantEntries > 0 then
        local trickplant = findMatchingTrickplantEntry(clientNum)
        if trickplant then
            if removeClosestDynamiteFromPlayer(clientNum) then
                chatAll(string.format(
                    '^3AUTOADMIN:^7 Dynamite planted by %s ^7was removed due to trickplant protection.',
                    getPlayerName(clientNum)
                ))
            end
            return
        end
    end
end

local function getObjectiveTakenSlot(text)
    local slot = string.match(text, '^Item:%s*(%d+)%s+team_CTF_redflag%s*$')
    if slot then
        return tonumber(slot)
    end

    slot = string.match(text, '^Item:%s*(%d+)%s+team_CTF_blueflag%s*$')
    if slot then
        return tonumber(slot)
    end

    return nil
end

local function handleObjectiveTake(clientNum)
    if #antirushEntries == 0 then
        return
    end

    if not isProtectionActive() then
        return
    end

    local antirush = findMatchingAntirushEntry(clientNum)
    if antirush then
        chatAll(string.format(
            '^3AUTOADMIN:^7 %s ^7was killed for rushing protected objective ^3%s^7.',
            getPlayerName(clientNum),
            antirush.shortname
        ))
        killPlayer(clientNum)
    end
end

local function showHelp(clientNum)
    printToClient(clientNum, "^3AUTOADMIN HELP")
    printToClient(clientNum, "aa help")
    printToClient(clientNum, "  Shows this help text.")
    printToClient(clientNum, "aa antirush")
    printToClient(clientNum, "  Lists nearby objective entities that can be protected against rushing.")
    printToClient(clientNum, "aa antirush [ID]")
    printToClient(clientNum, "  Saves the selected antirush objective.")
    printToClient(clientNum, "aa trickplant")
    printToClient(clientNum, "  Lists nearby dynamite entities that can be protected against trickplants.")
    printToClient(clientNum, "aa trickplant [ID]")
    printToClient(clientNum, "  Saves the selected trickplant area.")
    printToClient(clientNum, "aa list")
    printToClient(clientNum, "  Shows all saved antirush and trickplant entries.")
    printToClient(clientNum, "aa remove [NUMBER]")
    printToClient(clientNum, "  Removes one saved entry from the combined list.")
    printToClient(clientNum, "aa edit [NUMBER] [DISTANCE]")
    printToClient(clientNum, "  Changes the distance of one saved entry.")
end

function et_InitGame(levelTime, randomSeed, restart)
    et.G_Print(string.format("%s %s loaded\n", MODNAME, VERSION))
    loadAllowedGuids()
    loadProtectedFile()

    currentLevelTime = levelTime or 0
    protectionStartLevelTime = currentLevelTime
    announceDone = false
    announceAtLevelTime = currentLevelTime + 5000
    protectionExpiredAnnounced = false

    et.G_Print(string.format(
        "%s: protection starts at levelTime=%d and antirush lasts %.1f minutes\n",
        MODNAME,
        protectionStartLevelTime,
        getProtectMinutes()
    ))
end

function et_RunFrame(lvltime)
    currentLevelTime = lvltime or 0

    if not announceDone and currentLevelTime >= announceAtLevelTime then
        announceProtectedObjectives()
        announceDone = true
    end

    if announceDone and not protectionExpiredAnnounced and not isProtectionActive() then
        announceProtectionEnded()
        protectionExpiredAnnounced = true
    end
end

function et_Print(text)
    if not text then
        return
    end

    local plantSlot = string.match(text, DYNAMITE_PLANT_PATTERN)
    if plantSlot then
        handleDynamitePlant(tonumber(plantSlot))
        return
    end

    local takeSlot = getObjectiveTakenSlot(text)
    if takeSlot then
        handleObjectiveTake(takeSlot)
        return
    end
end

function et_ClientCommand(clientNum, command)
    local cmd0 = et.trap_Argv(0)
    local cmd1 = et.trap_Argv(1)
    local cmd2 = et.trap_Argv(2)
    local cmd3 = et.trap_Argv(3)

    if cmd0 ~= "aa" then
        return 0
    end

    if not requireAuthorization(clientNum) then
        return 1
    end

    if cmd1 == "help" or cmd1 == "" or cmd1 == nil then
        showHelp(clientNum)
        return 1
    end

    if cmd1 == "antirush" then
        if cmd2 and cmd2 ~= "" then
            saveAntirushById(clientNum, cmd2)
        else
            listAntirushCandidates(clientNum)
        end
        return 1
    end

    if cmd1 == "trickplant" then
        if cmd2 and cmd2 ~= "" then
            saveTrickplantById(clientNum, cmd2)
        else
            listTrickplantCandidates(clientNum)
        end
        return 1
    end

    if cmd1 == "list" then
        listSavedEntries(clientNum)
        return 1
    end

    if cmd1 == "remove" then
        if cmd2 and cmd2 ~= "" then
            removeSavedEntry(clientNum, cmd2)
        else
            printToClient(clientNum, "^1aa remove:^7 usage: aa remove [NUMBER]")
        end
        return 1
    end

    if cmd1 == "edit" then
        if cmd2 and cmd2 ~= "" and cmd3 and cmd3 ~= "" then
            editSavedEntryDistance(clientNum, cmd2, cmd3)
        else
            printToClient(clientNum, "^1aa edit:^7 usage: aa edit [NUMBER] [DISTANCE]")
        end
        return 1
    end

    showHelp(clientNum)
    return 1
end
The ingame setup is available at moment for slot0 (should still be the first private slot) and if the guid is in the specified file.

Yeah i am sometimes bored in the train.
Have a good day