Add Multidirectional Airstrafe and fix some projectile nospread issues

This commit is contained in:
BenCat07 2021-08-14 21:43:48 +02:00 committed by LightCat
parent 7bd167f43e
commit 3fc68c9c5c
5 changed files with 286 additions and 171 deletions

View File

@ -1,9 +1,15 @@
<Tab name="Movement" padding="6 6 6 6">
<Box padding="12 6 6 6" width="content" height="content" name="Bunny hop">
<Box padding="12 6 6 6" width="content" height="content" name="General">
<List width="150">
<AutoVariable width="fill" target="bunnyhop.enable" label="Enable bunny hop"/>
<AutoVariable width="fill" target="bunnyhop.chance" label="Bhop chance" tooltip="% chance of a successful bunnyhop."/>
<AutoVariable width="fill" target="misc.autostrafe" label="Enable auto strafe"/>
<LabeledObject width="fill" label="Auto strafe">
<Select target="misc.autostrafe">
<Option name="Off" value="0"/>
<Option name="Regular" value="1"/>
<Option name="Multidir" value="2"/>
</Select>
</LabeledObject>
</List>
</Box>
<Box padding="12 6 6 6" width="content" height="content" name="FollowBot" x="170">

View File

@ -28,7 +28,7 @@ namespace hacks::shared::misc
static settings::Boolean render_zoomed{ "visual.render-local-zoomed", "true" };
#endif
static settings::Boolean anti_afk{ "misc.anti-afk", "false" };
static settings::Boolean auto_strafe{ "misc.autostrafe", "false" };
static settings::Int auto_strafe{ "misc.autostrafe", "0" };
static settings::Boolean tauntslide{ "misc.tauntslide-tf2c", "false" };
static settings::Boolean tauntslide_tf2{ "misc.tauntslide", "false" };
static settings::Boolean flashlight_spam{ "misc.flashlight-spam", "false" };
@ -182,6 +182,29 @@ void DrawWireframeHitbox(wireframe_data data)
}
#endif
static float normalizeRad(float a) noexcept
{
return std::isfinite(a) ? std::remainder(a, PI * 2) : 0.0f;
}
static float angleDiffRad(float a1, float a2) noexcept
{
float delta;
delta = normalizeRad(a1 - a2);
if (a1 > a2)
{
if (delta >= PI)
delta -= PI * 2;
}
else
{
if (delta <= -PI)
delta += PI * 2;
}
return delta;
}
void CreateMove()
{
#if ENABLE_VISUALS
@ -206,18 +229,85 @@ void CreateMove()
// Automatically strafes in the air
if (auto_strafe && CE_GOOD(LOCAL_E) && !g_pLocalPlayer->life_state)
{
static bool was_jumping = false;
auto flags = CE_INT(LOCAL_E, netvar.iFlags);
bool is_jumping = current_user_cmd->buttons & IN_JUMP;
auto flags = CE_INT(LOCAL_E, netvar.iFlags);
auto movetype = (unsigned) CE_VAR(LOCAL_E, 0x194, unsigned char);
if (!(flags & FL_ONGROUND) && !(flags & FL_INWATER) && (!is_jumping || was_jumping))
// Noclip
if (movetype != 8)
{
if (current_user_cmd->mousedx)
switch (*auto_strafe)
{
current_user_cmd->sidemove = current_user_cmd->mousedx > 1 ? 450.f : -450.f;
case 0: // Off
return;
break;
case 1: // Regular strafe
{
static bool was_jumping = false;
bool is_jumping = current_user_cmd->buttons & IN_JUMP;
if (!(flags & FL_ONGROUND) || !(flags & FL_INWATER) && (!is_jumping || was_jumping))
if (current_user_cmd->mousedx)
current_user_cmd->sidemove = current_user_cmd->mousedx > 1 ? 450.f : -450.f;
was_jumping = is_jumping;
break;
}
case 2: // Multidirectional Airstrafe,
// Huge Credits to https://github.com/degeneratehyperbola/NEPS, as their airstrafe
// Apparently just works for tf2
// Also credits to "zelta" for porting it to tf2,
// And "Cyanide" for making it work with cathook.
{
static bool lastHeldJump = current_user_cmd->buttons & IN_JUMP;
const float speed = CE_VECTOR(LOCAL_E, netvar.vVelocity).Length2D();
auto vel = CE_VECTOR(LOCAL_E, netvar.vVelocity);
if (flags & FL_ONGROUND || flags & FL_INWATER)
return;
if (current_user_cmd->buttons & IN_JUMP)
return;
if (~current_user_cmd->buttons & current_user_cmd->buttons & IN_JUMP && !lastHeldJump)
return;
if (speed < 2.0f)
return;
constexpr auto perfectDelta = [](float speed) noexcept
{
static auto speedVar = CE_FLOAT(LOCAL_E, netvar.m_flMaxspeed);
static auto airVar = g_ICvar->FindVar("sv_airaccelerate");
// This is hardcoded for tf2, unless you run sourcemod
static auto wishSpeed = 30.0f;
const auto term = wishSpeed / airVar->GetFloat() / speedVar * 100.0f / speed;
if (term < 1.0f && term > -1.0f)
return acosf(term);
return 0.0f;
};
const float pDelta = perfectDelta(speed);
if (pDelta)
{
const float yaw = DEG2RAD(g_pLocalPlayer->v_OrigViewangles.y);
const float velDir = atan2f(vel.y, vel.x) - yaw;
const float wishAng = atan2f(-current_user_cmd->sidemove, current_user_cmd->forwardmove);
const float delta = angleDiffRad(velDir, wishAng); // Helpers::angleDiffRad(velDir, wishAng);
float moveDir = delta < 0.0f ? velDir + pDelta : velDir - pDelta;
current_user_cmd->forwardmove = cosf(moveDir) * 450.0f;
current_user_cmd->sidemove = -sinf(moveDir) * 450.0f;
}
lastHeldJump = current_user_cmd->buttons & current_user_cmd->buttons & IN_JUMP;
break;
}
default:
break;
}
}
was_jumping = is_jumping;
}
// TF2c Tauntslide
@ -386,6 +476,7 @@ void Draw()
if (local->m_iClassID() == CL_CLASS(CTFMinigun))
AddSideString(format("Weapon state: ", CE_INT(local, netvar.iWeaponState)));
AddSideString(format("ItemDefinitionIndex: ", CE_INT(local, netvar.iItemDefinitionIndex)));
AddSideString(format("Maxspeed: ", CE_FLOAT(LOCAL_E, netvar.m_flMaxspeed)));
/*AddSideString(colors::white, "Weapon: %s [%i]",
RAW_ENT(g_pLocalPlayer->weapon())->GetClientClass()->GetName(),
g_pLocalPlayer->weapon()->m_iClassID());
@ -563,6 +654,16 @@ CatCommand set_value("set", "Set value",
var->SetValue(value.c_str());
logging::Info("Set '%s' to '%s'", args.Arg(1), value.c_str());
});
CatCommand get_value("get", "Set value",
[](const CCommand &args)
{
if (args.ArgC() < 2)
return;
ConVar *var = g_ICvar->FindVar(args.Arg(1));
if (!var)
return;
logging::Info("'%s': '%s'", args.Arg(1), var->GetString());
});
CatCommand say_lines("say_lines", "Say with newlines (\\n)",
[](const CCommand &args)
{

View File

@ -1349,7 +1349,7 @@ bool GetProjectileData(CachedEntity *weapon, float &speed, float &gravity, float
}
case CL_CLASS(CTFSyringeGun):
{
rgrav = 0.2f;
rgrav = 0.3f;
rspeed = 1000.0f;
break;
}

View File

@ -29,7 +29,7 @@ static bool update_override_textures = false;
std::vector<std::string> world_strings = { "World" };
std::vector<std::string> skybox_strings = { "SkyBox" };
std::vector<std::string> gui_strings = { "Other", "VGUI" };
std::vector<std::string> dont_override_strings = { "glass", "door", "water", "tools", "player" };
std::vector<std::string> dont_override_strings = { "glass", "door", "water", "tools", "player", "wall28", "wall26" };
std::vector<std::string> nodraw_strings = { "decal", "overlay", "hay" };
namespace hooked_methods
@ -58,6 +58,8 @@ DEFINE_HOOKED_METHOD(FrameStageNotify, void, void *this_, ClientFrameStage_t sta
// Ensure world mat
if (name.find("World") == std::string::npos)
continue;
if (pMaterial->IsErrorMaterial() || !pMaterial->IsPrecached() || pMaterial->IsTranslucent() || pMaterial->IsSpriteCard())
continue;
// Don't override this stuff
bool good = true;
for (auto &entry : dont_override_strings)
@ -77,9 +79,9 @@ DEFINE_HOOKED_METHOD(FrameStageNotify, void, void *this_, ClientFrameStage_t sta
if (!pMaterial->GetMaterialVarFlag(MATERIAL_VAR_NO_DRAW))
{
auto *kv = new KeyValues(pMaterial->GetShaderName());
auto *kv = new KeyValues("LightmappedGeneric" /*pMaterial->GetShaderName()*/);
kv->SetString("$basetexture", (*override_textures_texture).c_str());
kv->SetString("$basetexturetransform", "center .5 .5 scale 6 6 rotate 0 translate 0 0");
//kv->SetString("$basetexturetransform", "center .5 .5 scale 6 6 rotate 0 translate 0 0");
kv->SetString("$surfaceprop", "concrete");
pMaterial->SetShaderAndParams(kv);
}

View File

@ -82,16 +82,7 @@ void CreateMove()
// Credits to https://www.unknowncheats.me/forum/team-fortress-2-a/139094-projectile-nospread.html
// Set up Random Seed
int cmd_num = current_user_cmd->command_number;
// Crithack uses different things
if (criticals::isEnabled() && g_pLocalPlayer->weapon_mode != weapon_melee && criticals::crit_cmds.find(LOCAL_W->m_IDX) != criticals::crit_cmds.end() && criticals::crit_cmds.find(LOCAL_W->m_IDX)->second.size())
{
int array_index = criticals::current_index;
if (array_index >= criticals::crit_cmds.at(LOCAL_W->m_IDX).size())
array_index = 0;
// Adjust for nospread
cmd_num = criticals::crit_cmds.at(LOCAL_W->m_IDX).at(array_index);
}
int cmd_num = current_late_user_cmd->command_number;
RandomSeed(MD5_PseudoRandom(cmd_num) & 0x7FFFFFFF);
SharedRandomInt(MD5_PseudoRandom(cmd_num) & 0x7FFFFFFF, "SelectWeightedSequence", 0, 0, 0);
for (int i = 0; i < 6; ++i)
@ -106,7 +97,7 @@ void CreateMove()
{
// Check if we released the barrage by releasing m1, also lock bool so people don't just release m1 and tap it again
if (!should_nospread)
should_nospread = !(current_user_cmd->buttons & IN_ATTACK) && g_pLocalPlayer->bAttackLastTick;
should_nospread = !(current_late_user_cmd->buttons & IN_ATTACK) && g_pLocalPlayer->bAttackLastTick;
if (!CE_INT(LOCAL_W, netvar.m_iClip1) && CE_INT(LOCAL_W, netvar.iReloadMode) == 0)
{
@ -121,29 +112,29 @@ void CreateMove()
// Huntsman check
else if (LOCAL_W->m_iClassID() == CL_CLASS(CTFCompoundBow))
{
if (!g_pLocalPlayer->bAttackLastTick || (current_user_cmd->buttons & IN_ATTACK))
if (!g_pLocalPlayer->bAttackLastTick || (current_late_user_cmd->buttons & IN_ATTACK))
return;
}
// Rest of weapons
else if (!(current_user_cmd->buttons & IN_ATTACK))
else if (!(current_late_user_cmd->buttons & IN_ATTACK))
return;
switch (LOCAL_W->m_iClassID())
{
case CL_CLASS(CTFSyringeGun):
{
if (g_pLocalPlayer->v_OrigViewangles == current_user_cmd->viewangles)
if (g_pLocalPlayer->v_OrigViewangles == current_late_user_cmd->viewangles)
g_pLocalPlayer->bUseSilentAngles = true;
float spread = 1.5f;
current_user_cmd->viewangles.x -= RandomFloat(-spread, spread);
current_user_cmd->viewangles.y -= RandomFloat(-spread, spread);
fClampAngle(current_user_cmd->viewangles);
current_late_user_cmd->viewangles.x -= RandomFloat(-spread, spread);
current_late_user_cmd->viewangles.y -= RandomFloat(-spread, spread);
fClampAngle(current_late_user_cmd->viewangles);
break;
}
case CL_CLASS(CTFCompoundBow):
{
Vector view = re::C_BasePlayer::GetLocalEyeAngles(RAW_ENT(LOCAL_E));
if (g_pLocalPlayer->v_OrigViewangles == current_user_cmd->viewangles)
if (g_pLocalPlayer->v_OrigViewangles == current_late_user_cmd->viewangles)
g_pLocalPlayer->bUseSilentAngles = true;
Vector spread;
@ -152,25 +143,25 @@ void CreateMove()
re::C_TFWeaponBase::GetProjectileFireSetupHuntsman(RAW_ENT(LOCAL_W), RAW_ENT(LOCAL_E), Vector(23.5f, -8.f, 8.f), &src, &spread, false, 2000.0f);
spread -= view;
current_user_cmd->viewangles -= spread;
fClampAngle(current_user_cmd->viewangles);
current_late_user_cmd->viewangles -= spread;
fClampAngle(current_late_user_cmd->viewangles);
break;
}
default:
Vector view = re::C_BasePlayer::GetLocalEyeAngles(RAW_ENT(LOCAL_E));
if (g_pLocalPlayer->v_OrigViewangles == current_user_cmd->viewangles)
if (g_pLocalPlayer->v_OrigViewangles == current_late_user_cmd->viewangles)
g_pLocalPlayer->bUseSilentAngles = true;
Vector spread = re::C_TFWeaponBase::GetSpreadAngles(RAW_ENT(LOCAL_W));
spread -= view;
current_user_cmd->viewangles -= spread;
fClampAngle(current_user_cmd->viewangles);
current_late_user_cmd->viewangles -= spread;
fClampAngle(current_late_user_cmd->viewangles);
break;
}
}
static InitRoutine init([]() { EC::Register(EC::CreateMove, CreateMove, "nospread_cm", EC::very_late); });
static InitRoutine init([]() { EC::Register(EC::CreateMoveLate, CreateMove, "nospread_cm", EC::very_late); });
enum nospread_sync_state
{
@ -306,57 +297,63 @@ void ApplySpreadCorrection(Vector &angles, int seed, float spread)
fClampAngle(angles);
}
CatCommand debug_mantissa("test_mantissa", "For debug purposes", [](const CCommand &rCmd) {
if (rCmd.ArgC() < 2)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "You must provide float to test.\n");
return;
}
CatCommand debug_mantissa("test_mantissa", "For debug purposes",
[](const CCommand &rCmd)
{
if (rCmd.ArgC() < 2)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "You must provide float to test.\n");
return;
}
try
{
float float_value = atof(rCmd.Arg(1));
float mantissa_step = CalculateMantissaStep(float_value);
try
{
float float_value = atof(rCmd.Arg(1));
float mantissa_step = CalculateMantissaStep(float_value);
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Mantissa step for %.3f: %.10f\n", float_value, mantissa_step);
}
catch (const std::invalid_argument &)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Invalid float.\n");
}
return;
});
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Mantissa step for %.3f: %.10f\n", float_value, mantissa_step);
}
catch (const std::invalid_argument &)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Invalid float.\n");
}
return;
});
static CatCommand nospread_sync("nospread_sync", "Try to sync client and server time", []() {
if (!bullet)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Set nospread.enable to true first.\n");
return;
}
should_update_time = true;
no_spread_synced = NOT_SYNCED;
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Trying to sync Seed...\n");
});
static CatCommand nospread_sync("nospread_sync", "Try to sync client and server time",
[]()
{
if (!bullet)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Set nospread.enable to true first.\n");
return;
}
should_update_time = true;
no_spread_synced = NOT_SYNCED;
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Trying to sync Seed...\n");
});
static CatCommand nospread_resync("nospread_resync", "Try to sync client and server time", []() {
if (!bullet)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Set nospread.enable to true first.\n");
return;
}
if (no_spread_synced == CORRECTING)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Already syncing.");
return;
}
if (no_spread_synced != SYNCED)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Can't resync when not previously synced! Use cat_nospread_sync\n");
return;
}
no_spread_synced = CORRECTING;
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Trying to resync Seed...\n");
});
static CatCommand nospread_resync("nospread_resync", "Try to sync client and server time",
[]()
{
if (!bullet)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Set nospread.enable to true first.\n");
return;
}
if (no_spread_synced == CORRECTING)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Already syncing.");
return;
}
if (no_spread_synced != SYNCED)
{
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Can't resync when not previously synced! Use cat_nospread_sync\n");
return;
}
no_spread_synced = CORRECTING;
g_ICvar->ConsoleColorPrintf(MENU_COLOR, "Trying to resync Seed...\n");
});
// Our Detour hooks
DetourHook cl_writeusercmd_detour;
@ -627,7 +624,8 @@ void CL_SendMove_hook()
int new_packets = 1 + *choked_packets;
auto RecheckIfresync_needed = [&new_packets](double asumed_time) -> void {
auto RecheckIfresync_needed = [&new_packets](double asumed_time) -> void
{
static Timer s_NextCheck;
// we use it as 1 sec delay
if (s_NextCheck.check(1000) && new_packets == 1 && (no_spread_synced != SYNCED || !LOCAL_E->m_bAlivePlayer()) && !waiting_perf_data)
@ -879,101 +877,109 @@ void CreateMove2()
}
}
static InitRoutine init_bulletnospread([]() {
// Get our detour hooks running
static auto writeusercmd_addr = gSignatures.GetClientSignature("55 89 E5 57 56 53 83 EC 2C 8B 45 ? 8B 7D ? 8B 5D ? 89 45 ? 8B 40");
cl_writeusercmd_detour.Init(writeusercmd_addr, (void *) WriteUserCmd_hook);
static auto fx_firebullets_addr = gSignatures.GetClientSignature("55 89 E5 57 56 53 81 EC 0C 01 00 00 8B 45 ? 8B 7D ? 89 85");
fx_firebullets_detour.Init(fx_firebullets_addr, (void *) FX_FireBullets_hook);
/*static auto net_sendpacket_addr = gSignatures.GetEngineSignature("55 89 E5 57 56 53 81 EC EC 20 00 00 C7 85 ? ? ? ? 00 00 00 00 8B 45");
net_sendpacket_detour.Init(net_sendpacket_addr, (void *) NET_SendPacket_hook);*/
static InitRoutine init_bulletnospread(
[]()
{
// Get our detour hooks running
static auto writeusercmd_addr = gSignatures.GetClientSignature("55 89 E5 57 56 53 83 EC 2C 8B 45 ? 8B 7D ? 8B 5D ? 89 45 ? 8B 40");
cl_writeusercmd_detour.Init(writeusercmd_addr, (void *) WriteUserCmd_hook);
static auto fx_firebullets_addr = gSignatures.GetClientSignature("55 89 E5 57 56 53 81 EC 0C 01 00 00 8B 45 ? 8B 7D ? 89 85");
fx_firebullets_detour.Init(fx_firebullets_addr, (void *) FX_FireBullets_hook);
/*static auto net_sendpacket_addr = gSignatures.GetEngineSignature("55 89 E5 57 56 53 81 EC EC 20 00 00 C7 85 ? ? ? ? 00 00 00 00 8B 45");
net_sendpacket_detour.Init(net_sendpacket_addr, (void *) NET_SendPacket_hook);*/
// Register Event callbacks
EC::Register(EC::CreateMove, CreateMove2, "nospread_createmove2");
EC::Register(EC::CreateMoveWarp, CreateMove2, "nospread_createmove2w");
// Register Event callbacks
EC::Register(EC::CreateMove, CreateMove2, "nospread_createmove2");
EC::Register(EC::CreateMoveWarp, CreateMove2, "nospread_createmove2w");
bullet.installChangeCallback([](settings::VariableBase<bool> &, bool after) {
if (!after)
{
is_syncing = false;
no_spread_synced = NOT_SYNCED;
}
});
#if ENABLE_VISUALS
EC::Register(
EC::Draw,
[]() {
if (bullet && (draw || draw_mantissa) && CE_GOOD(LOCAL_E) && LOCAL_E->m_bAlivePlayer())
bullet.installChangeCallback(
[](settings::VariableBase<bool> &, bool after)
{
std::string draw_string = "";
rgba_t draw_color = colors::white;
switch (no_spread_synced)
if (!after)
{
case NOT_SYNCED:
is_syncing = false;
no_spread_synced = NOT_SYNCED;
}
});
#if ENABLE_VISUALS
EC::Register(
EC::Draw,
[]()
{
if (bullet && (draw || draw_mantissa) && CE_GOOD(LOCAL_E) && LOCAL_E->m_bAlivePlayer())
{
if (bad_mantissa)
std::string draw_string = "";
rgba_t draw_color = colors::white;
switch (no_spread_synced)
{
draw_color = colors::red_s;
draw_string = "Server uptime too Low!";
}
else
case NOT_SYNCED:
{
draw_color = colors::orange;
draw_string = "Not Syncing";
if (bad_mantissa)
{
draw_color = colors::red_s;
draw_string = "Server uptime too Low!";
}
else
{
draw_color = colors::orange;
draw_string = "Not Syncing";
}
break;
}
break;
case CORRECTING:
case DEAD_SYNC:
{
draw_color = colors::yellow;
draw_string = "Syncing...";
break;
}
case SYNCED:
{
draw_color = colors::green;
draw_string = "Synced.";
break;
}
default:
break;
}
if (draw)
AddCenterString(draw_string, draw_color);
if (draw_mantissa && no_spread_synced != NOT_SYNCED)
AddCenterString("Mantissa step size: " + std::to_string((int) CalculateMantissaStep(1000.0 * (Plat_FloatTime() + float_time_delta))), draw_color);
}
case CORRECTING:
case DEAD_SYNC:
{
draw_color = colors::yellow;
draw_string = "Syncing...";
break;
}
case SYNCED:
{
draw_color = colors::green;
draw_string = "Synced.";
break;
}
default:
break;
}
if (draw)
AddCenterString(draw_string, draw_color);
if (draw_mantissa && no_spread_synced != NOT_SYNCED)
AddCenterString("Mantissa step size: " + std::to_string((int) CalculateMantissaStep(1000.0 * (Plat_FloatTime() + float_time_delta))), draw_color);
}
},
"nospread_draw");
},
"nospread_draw");
#endif
EC::Register(
EC::LevelInit,
[]() {
no_spread_synced = NOT_SYNCED;
last_was_player_perf = false;
bad_mantissa = false;
waiting_perf_data = false;
},
"nospread_levelinit");
EC::Register(
EC::LevelInit,
[]()
{
no_spread_synced = NOT_SYNCED;
last_was_player_perf = false;
bad_mantissa = false;
waiting_perf_data = false;
},
"nospread_levelinit");
EC::Register(
EC::LevelShutdown,
[]() {
no_spread_synced = NOT_SYNCED;
last_was_player_perf = false;
bad_mantissa = false;
waiting_perf_data = false;
},
"nospread_levelshutdown");
EC::Register(
EC::Shutdown,
[]() {
cl_writeusercmd_detour.Shutdown();
fx_firebullets_detour.Shutdown();
// net_sendpacket_detour.Shutdown();
},
"nospread_shutdown");
});
EC::Register(
EC::LevelShutdown,
[]()
{
no_spread_synced = NOT_SYNCED;
last_was_player_perf = false;
bad_mantissa = false;
waiting_perf_data = false;
},
"nospread_levelshutdown");
EC::Register(
EC::Shutdown,
[]()
{
cl_writeusercmd_detour.Shutdown();
fx_firebullets_detour.Shutdown();
// net_sendpacket_detour.Shutdown();
},
"nospread_shutdown");
});
} // namespace hacks::tf2::nospread