/*
* CSB Map Spawn Editor
* Copyright (C) 2026 counter-strike-boost.com
*
* In-game spawn editor for admins. /addspawn stores your current origin and
* angles, /delspawn removes the closest saved point, /savespawns writes them,
* and /listspawns reports the count. Spawns are kept per map in the AMXX data
* directory. On respawn the plugin teleports the player to a saved point with
* engfunc(EngFunc_SetOrigin) - useful for DM and CSDM style maps.
*
* Inspired by the CSDM spawn editor by BAILOPAN. 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 for details.
*/
#include
#include
#include
#include
#define MAX_SPAWNS 64
new const PLUGIN[] = "CSB Map Spawn Editor"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
new Float:g_fSpawn[MAX_SPAWNS][6]
new g_iSpawnCount = 0
new g_pEnabled, g_pOverride
new g_szFile[192]
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_spawn_enabled", "1")
g_pOverride = register_cvar("csb_spawn_override", "1")
register_clcmd("say /addspawn", "cmdAdd")
register_clcmd("say /delspawn", "cmdDel")
register_clcmd("say /savespawns", "cmdSave")
register_clcmd("say /listspawns", "cmdList")
RegisterHam(Ham_Spawn, "player", "fwdSpawnPost", 1)
}
public plugin_cfg()
{
new dir[128]
get_localinfo("amxx_datadir", dir, charsmax(dir))
new map[40]
get_mapname(map, charsmax(map))
formatex(g_szFile, charsmax(g_szFile), "%s/csb_spawns_%s.txt", dir, map)
loadSpawns()
}
loadSpawns()
{
g_iSpawnCount = 0
if (!file_exists(g_szFile))
return
new f = fopen(g_szFile, "rt")
if (!f)
return
new line[128], p[6][16]
while (!feof(f) && g_iSpawnCount < MAX_SPAWNS)
{
fgets(f, line, charsmax(line))
trim(line)
if (!line[0] || line[0] == ';')
continue
if (parse(line, p[0], 15, p[1], 15, p[2], 15, p[3], 15, p[4], 15, p[5], 15) < 6)
continue
for (new j = 0; j < 6; j++)
g_fSpawn[g_iSpawnCount][j] = str_to_float(p[j])
g_iSpawnCount++
}
fclose(f)
log_amx("[CSB Spawn] loaded %d spawn(s) for this map.", g_iSpawnCount)
}
bool:hasAccess(id)
{
if (get_user_flags(id) & ADMIN_MAP)
return true
client_print(id, print_chat, "[CSB] You need the map admin flag (e) to edit spawns.")
return false
}
public cmdAdd(id)
{
if (!hasAccess(id))
return PLUGIN_HANDLED
if (g_iSpawnCount >= MAX_SPAWNS)
{
client_print(id, print_chat, "[CSB] Spawn list is full (%d).", MAX_SPAWNS)
return PLUGIN_HANDLED
}
new Float:origin[3], Float:angles[3]
pev(id, pev_origin, origin)
pev(id, pev_v_angle, angles)
g_fSpawn[g_iSpawnCount][0] = origin[0]
g_fSpawn[g_iSpawnCount][1] = origin[1]
g_fSpawn[g_iSpawnCount][2] = origin[2]
g_fSpawn[g_iSpawnCount][3] = angles[0]
g_fSpawn[g_iSpawnCount][4] = angles[1]
g_fSpawn[g_iSpawnCount][5] = angles[2]
g_iSpawnCount++
client_print(id, print_chat, "[CSB] Spawn #%d saved at your position. Use /savespawns to write to disk.", g_iSpawnCount)
return PLUGIN_HANDLED
}
public cmdDel(id)
{
if (!hasAccess(id))
return PLUGIN_HANDLED
if (!g_iSpawnCount)
{
client_print(id, print_chat, "[CSB] No spawns to delete.")
return PLUGIN_HANDLED
}
new Float:origin[3]
pev(id, pev_origin, origin)
new closest = -1
new Float:best = 999999.0
for (new i = 0; i < g_iSpawnCount; i++)
{
new Float:pt[3]
pt[0] = g_fSpawn[i][0]
pt[1] = g_fSpawn[i][1]
pt[2] = g_fSpawn[i][2]
new Float:d = get_distance_f(origin, pt)
if (d < best)
{
best = d
closest = i
}
}
if (closest < 0)
return PLUGIN_HANDLED
/* shift the rest down */
for (new i = closest; i < g_iSpawnCount - 1; i++)
{
for (new j = 0; j < 6; j++)
g_fSpawn[i][j] = g_fSpawn[i + 1][j]
}
g_iSpawnCount--
client_print(id, print_chat, "[CSB] Removed the closest spawn (%.0f units away). %d left.", best, g_iSpawnCount)
return PLUGIN_HANDLED
}
public cmdSave(id)
{
if (!hasAccess(id))
return PLUGIN_HANDLED
new f = fopen(g_szFile, "wt")
if (!f)
{
client_print(id, print_chat, "[CSB] Could not open the spawn file for writing.")
return PLUGIN_HANDLED
}
fprintf(f, "; CSB Map Spawn Editor - x y z pitch yaw roll^n")
for (new i = 0; i < g_iSpawnCount; i++)
{
fprintf(f, "%.2f %.2f %.2f %.2f %.2f %.2f^n",
g_fSpawn[i][0], g_fSpawn[i][1], g_fSpawn[i][2],
g_fSpawn[i][3], g_fSpawn[i][4], g_fSpawn[i][5])
}
fclose(f)
client_print(id, print_chat, "[CSB] Wrote %d spawn(s) to disk.", g_iSpawnCount)
log_amx("[CSB Spawn] admin saved %d spawn(s).", g_iSpawnCount)
return PLUGIN_HANDLED
}
public cmdList(id)
{
if (!hasAccess(id))
return PLUGIN_HANDLED
client_print(id, print_chat, "[CSB] %d custom spawn(s) loaded for this map.", g_iSpawnCount)
return PLUGIN_HANDLED
}
public fwdSpawnPost(id)
{
if (!get_pcvar_num(g_pEnabled) || !get_pcvar_num(g_pOverride))
return
if (!g_iSpawnCount || !is_user_alive(id))
return
set_task(0.1, "taskTeleport", id)
}
public taskTeleport(id)
{
if (!is_user_alive(id) || !g_iSpawnCount)
return
new pick = random_num(0, g_iSpawnCount - 1)
new Float:origin[3], Float:angles[3]
origin[0] = g_fSpawn[pick][0]
origin[1] = g_fSpawn[pick][1]
origin[2] = g_fSpawn[pick][2]
angles[0] = g_fSpawn[pick][3]
angles[1] = g_fSpawn[pick][4]
angles[2] = g_fSpawn[pick][5]
engfunc(EngFunc_SetOrigin, id, origin)
set_pev(id, pev_angles, angles)
set_pev(id, pev_v_angle, angles)
set_pev(id, pev_fixangle, 1)
/* stop any residual momentum from the map's default spawn */
new Float:zero[3]
set_pev(id, pev_velocity, zero)
}