/*
* CSB Unstuck
* Copyright (C) 2026 counter-strike-boost.com
*
* "say /unstuck" frees a player wedged inside map geometry or another player.
* The plugin confirms the player is actually stuck with a human-hull trace at
* his current position, then searches outward in a growing sphere for the
* nearest empty spot (PointContents + a hull trace that does not start solid)
* and teleports him there. A cooldown and a combat lock stop it being abused
* to escape a firefight.
*
* Inspired by the many community "Stuck / Unstuck" plugins for AMX Mod X. This
* is an independent GPL re-implementation; no original code is reused.
*
* 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
new const PLUGIN[] = "CSB Unstuck"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define HULL_HUMAN 1
#define DONT_IGNORE_MONSTERS 0
#define CONTENTS_EMPTY -1
new g_pEnabled, g_pCooldown, g_pCombatLock, g_pMaxDist
new g_iLastUse[33]
new g_iLastDamage[33]
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_unstuck", "1")
g_pCooldown = register_cvar("csb_unstuck_cooldown", "10")
g_pCombatLock = register_cvar("csb_unstuck_combatlock", "5")
g_pMaxDist = register_cvar("csb_unstuck_maxdist", "128")
register_clcmd("say /unstuck", "cmdUnstuck")
register_clcmd("say_team /unstuck", "cmdUnstuck")
register_clcmd("say /stuck", "cmdUnstuck")
register_event("Damage", "onDamage", "b")
}
public onDamage(id)
{
if (read_data(2) > 0)
g_iLastDamage[id] = get_systime()
}
public client_disconnected(id)
{
g_iLastUse[id] = 0
g_iLastDamage[id] = 0
}
public cmdUnstuck(id)
{
if (!get_pcvar_num(g_pEnabled))
return PLUGIN_HANDLED
if (!is_user_alive(id))
{
client_print(id, print_chat, "[CSB] You have to be alive to use /unstuck.")
return PLUGIN_HANDLED
}
new now = get_systime()
new lock = get_pcvar_num(g_pCombatLock)
if (lock > 0 && (now - g_iLastDamage[id]) < lock)
{
client_print(id, print_chat, "[CSB] You cannot unstuck while in combat. Wait %d second(s).",
lock - (now - g_iLastDamage[id]))
return PLUGIN_HANDLED
}
new cd = get_pcvar_num(g_pCooldown)
if (cd > 0 && g_iLastUse[id] && (now - g_iLastUse[id]) < cd)
{
client_print(id, print_chat, "[CSB] /unstuck is on cooldown for %d more second(s).",
cd - (now - g_iLastUse[id]))
return PLUGIN_HANDLED
}
new Float:origin[3]
pev(id, pev_origin, origin)
if (!isStuck(id, origin))
{
client_print(id, print_chat, "[CSB] You are not stuck.")
return PLUGIN_HANDLED
}
new Float:dest[3]
if (findFreeSpot(id, origin, dest))
{
set_pev(id, pev_origin, dest)
set_pev(id, pev_velocity, Float:{0.0, 0.0, 0.0})
g_iLastUse[id] = now
client_print(id, print_chat, "[CSB] You have been freed.")
}
else
{
client_print(id, print_chat, "[CSB] Could not find a free spot nearby. Try again in a moment.")
}
return PLUGIN_HANDLED
}
bool:isStuck(id, const Float:pos[3])
{
new tr = 0
engfunc(EngFunc_TraceHull, pos, pos, DONT_IGNORE_MONSTERS, HULL_HUMAN, id, tr)
if (get_tr2(tr, TR_StartSolid) || get_tr2(tr, TR_AllSolid))
return true
return false
}
/* Spiral out from the origin sampling candidate points until one is free. */
bool:findFreeSpot(id, const Float:origin[3], Float:out[3])
{
new Float:maxDist = get_pcvar_float(g_pMaxDist)
new Float:cand[3]
new tr = 0
/* 8 compass directions on the horizontal plane, plus straight up. */
static const Float:dirs[9][3] =
{
{ 1.0, 0.0, 0.0 },
{ -1.0, 0.0, 0.0 },
{ 0.0, 1.0, 0.0 },
{ 0.0, -1.0, 0.0 },
{ 0.7, 0.7, 0.0 },
{ 0.7, -0.7, 0.0 },
{ -0.7, 0.7, 0.0 },
{ -0.7, -0.7, 0.0 },
{ 0.0, 0.0, 1.0 }
}
for (new Float:radius = 24.0; radius <= maxDist; radius += 16.0)
{
for (new d = 0; d < sizeof(dirs); d++)
{
cand[0] = origin[0] + dirs[d][0] * radius
cand[1] = origin[1] + dirs[d][1] * radius
cand[2] = origin[2] + dirs[d][2] * radius
if (engfunc(EngFunc_PointContents, cand) != CONTENTS_EMPTY)
continue
engfunc(EngFunc_TraceHull, cand, cand, DONT_IGNORE_MONSTERS, HULL_HUMAN, id, tr)
if (get_tr2(tr, TR_StartSolid) || get_tr2(tr, TR_AllSolid))
continue
out[0] = cand[0]
out[1] = cand[1]
out[2] = cand[2]
return true
}
}
return false
}