/*
* CSB Capture The Flag
* Copyright (C) 2026 counter-strike-boost.com
*
* A capture-the-flag layer for any map. Two flag entities are spawned at base
* coordinates read from a per-map data file (addons/amxmodx/configs/csb_ctf/
* <map>.ini, two "x y z" lines: Terrorist base then CT base). Touching the enemy
* flag picks it up (Ham_Touch); it then follows the carrier. Bringing it to your
* own base while your flag is home scores a capture. Dropped flags auto-return,
* and captures are tracked on the HUD.
*
* Inspired by CTF by Avalanche. Independent GPL re-implementation.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version. It is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See <https://www.gnu.org/licenses/> for details.
*/
#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
new const PLUGIN[] = "CSB Capture The Flag"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define FLAG_CLASS "csb_ctf_flag"
#define FLAG_MODEL "models/w_backpack.mdl"
#define CAP_RANGE2 6400.0 /* 80 units */
new g_pEnabled
new g_iFlagEnt[2]
new Float:g_fHome[2][3]
new g_iCarrier[2]
new bool:g_bAtHome[2]
new g_iScore[2]
new bool:g_bReady
new g_iHudSync
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_ctf_enabled", "1")
RegisterHam(Ham_Touch, FLAG_CLASS, "fwFlagTouch")
register_event("DeathMsg", "eventDeath", "a", "1>0")
g_iHudSync = CreateHudSyncObj()
set_task(0.1, "taskFlags", 0, _, _, "b")
}
public plugin_precache()
{
engfunc(EngFunc_PrecacheModel, FLAG_MODEL)
}
public plugin_cfg()
{
if (loadBases())
spawnFlags()
}
bool:loadBases()
{
new map[32]
get_mapname(map, charsmax(map))
new dir[128], path[192]
get_localinfo("amxx_configsdir", dir, charsmax(dir))
if (!dir[0])
copy(dir, charsmax(dir), "addons/amxmodx/configs")
formatex(path, charsmax(path), "%s/csb_ctf/%s.ini", dir, map)
if (!file_exists(path))
return false
new line[96], len, pos = 0, n = 0
while (n < 2 && (pos = read_file(path, pos, line, charsmax(line), len)))
{
trim(line)
if (!line[0] || line[0] == ';' || line[0] == '#')
continue
new sx[16], sy[16], sz[16]
parse(line, sx, charsmax(sx), sy, charsmax(sy), sz, charsmax(sz))
if (!sx[0] || !sy[0] || !sz[0])
continue
g_fHome[n][0] = str_to_float(sx)
g_fHome[n][1] = str_to_float(sy)
g_fHome[n][2] = str_to_float(sz)
n++
}
return (n == 2)
}
spawnFlags()
{
for (new i = 0; i < 2; i++)
{
new ent = engfunc(EngFunc_CreateNamedEntity, engfunc(EngFunc_AllocString, "info_target"))
if (!ent)
return
set_pev(ent, pev_classname, FLAG_CLASS)
engfunc(EngFunc_SetModel, ent, FLAG_MODEL)
set_pev(ent, pev_solid, SOLID_TRIGGER)
set_pev(ent, pev_movetype, MOVETYPE_NONE)
set_pev(ent, pev_iuser1, i)
engfunc(EngFunc_SetOrigin, ent, g_fHome[i])
g_iFlagEnt[i] = ent
g_iCarrier[i] = 0
g_bAtHome[i] = true
}
g_bReady = true
}
/* flag i belongs to team (i==0 -> T(1), i==1 -> CT(2)) */
flagTeam(i)
{
return i == 0 ? 1 : 2
}
public fwFlagTouch(flag, other)
{
if (!g_bReady || other < 1 || other > 32 || !is_user_alive(other))
return HAM_IGNORED
new i = pev(flag, pev_iuser1)
if (i < 0 || i > 1)
return HAM_IGNORED
new team = get_user_team(other)
/* touching your own flag returns it if it was dropped away from home */
if (team == flagTeam(i))
{
if (!g_bAtHome[i] && g_iCarrier[i] == 0)
returnFlag(i)
return HAM_IGNORED
}
/* enemy flag: pick it up */
if (g_iCarrier[i] == 0)
{
g_iCarrier[i] = other
g_bAtHome[i] = false
new name[32]
get_user_name(other, name, charsmax(name))
client_print(0, print_chat, "^x04[CTF]^x01 %s picked up the %s flag!", name, i == 0 ? "Terrorist" : "CT")
}
return HAM_IGNORED
}
returnFlag(i)
{
g_iCarrier[i] = 0
g_bAtHome[i] = true
if (pev_valid(g_iFlagEnt[i]))
engfunc(EngFunc_SetOrigin, g_iFlagEnt[i], g_fHome[i])
}
public taskFlags()
{
if (!g_bReady || !get_pcvar_num(g_pEnabled))
return
for (new i = 0; i < 2; i++)
{
new c = g_iCarrier[i]
if (c == 0 || !pev_valid(g_iFlagEnt[i]))
continue
if (!is_user_alive(c))
{
dropFlag(i)
continue
}
/* flag follows the carrier */
static Float:fPos[3]
pev(c, pev_origin, fPos)
fPos[2] += 20.0
engfunc(EngFunc_SetOrigin, g_iFlagEnt[i], fPos)
/* capture check: carrier at their own base with own flag home */
new own = get_user_team(c) == 1 ? 0 : 1
if (g_bAtHome[own])
{
static Float:fMe[3]
pev(c, pev_origin, fMe)
new Float:dx = fMe[0] - g_fHome[own][0]
new Float:dy = fMe[1] - g_fHome[own][1]
new Float:dz = fMe[2] - g_fHome[own][2]
if (dx*dx + dy*dy + dz*dz < CAP_RANGE2)
capture(c, i, own)
}
}
drawHud()
}
capture(player, enemyFlag, ownIndex)
{
g_iScore[ownIndex]++
new name[32]
get_user_name(player, name, charsmax(name))
client_print(0, print_chat, "^x04[CTF]^x01 %s captured the flag! Score - T:%d CT:%d", name, g_iScore[0], g_iScore[1])
returnFlag(enemyFlag)
}
dropFlag(i)
{
new c = g_iCarrier[i]
g_iCarrier[i] = 0
if (c >= 1 && c <= 32)
{
static Float:fPos[3]
pev(c, pev_origin, fPos)
engfunc(EngFunc_SetOrigin, g_iFlagEnt[i], fPos)
}
/* auto-return after a delay */
set_task(12.0, "taskAutoReturn", 3000 + i)
}
public taskAutoReturn(taskid)
{
new i = taskid - 3000
if (i >= 0 && i <= 1 && g_iCarrier[i] == 0 && !g_bAtHome[i])
{
returnFlag(i)
client_print(0, print_chat, "^x04[CTF]^x01 The %s flag returned to base.", i == 0 ? "Terrorist" : "CT")
}
}
public eventDeath()
{
new victim = read_data(2)
for (new i = 0; i < 2; i++)
if (g_iCarrier[i] == victim)
dropFlag(i)
}
public client_disconnected(id)
{
for (new i = 0; i < 2; i++)
if (g_iCarrier[i] == id)
dropFlag(i)
}
drawHud()
{
set_hudmessage(0, 200, 255, 0.02, 0.10, 0, 0.0, 0.3, 0.0, 0.0, -1)
ShowSyncHudMsg(0, g_iHudSync, "CTF T:%d CT:%d", g_iScore[0], g_iScore[1])
}