From 43f5ae079d1ad490da1f9f393c7bfca191cec5c6 Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Mon, 2 Mar 2026 23:09:35 -0700 Subject: [PATCH 1/6] Adds current dodgeball plugins & config --- .gitmodules | 3 + Makefile | 68 -- justfile | 64 ++ .../addons/sourcemod/scripting/dodgeball.sp | 580 ++++++++++ .../sourcemod/scripting/include/colors.inc | 937 ++++++++++++----- .../scripting/include/dodgeball_config.inc | 581 ++++++++++ .../scripting/include/dodgeball_core.inc | 200 ++++ .../scripting/include/dodgeball_events.inc | 453 ++++++++ .../scripting/include/dodgeball_natives.inc | 810 ++++++++++++++ .../scripting/include/dodgeball_rockets.inc | 746 +++++++++++++ .../scripting/include/dodgeball_utilities.inc | 373 +++++++ .../scripting/include/morecolors.inc | 131 ++- .../scripting/include/multicolors.inc | 461 ++++++++ .../sourcemod/scripting/include/tfdb.inc | 994 ++++++++++++++++++ .../scripting/tfdb_airblast_prevention.sp | 185 ++++ .../addons/sourcemod/scripting/tfdb_ffa.sp | 615 +++++++++++ .../sourcemod/scripting/tfdb_no_block.sp | 68 ++ .../addons/sourcemod/scripting/tfdb_print.sp | 262 +++++ .../sourcemod/scripting/tfdb_speedhud.sp | 343 ++++++ .../addons/sourcemod/scripting/tfdb_votes.sp | 713 +++++++++++++ .../addons/sourcemod/scripting/thirdperson.sp | 74 ++ .../sourcemod/translations/tfdb.phrases.txt | 644 ++++++++++++ .../files/cfg/sourcemod/dodgeball_disable.cfg | 14 + .../files/cfg/sourcemod/dodgeball_enable.cfg | 14 + .../cfg/sourcemod/dodgeball_ffa_disable.cfg | 2 + .../cfg/sourcemod/dodgeball_ffa_enable.cfg | 2 + roles/srcds/tasks/main.yml | 24 + .../srcds/templates/dodgeball_general.cfg.j2 | 313 ++++++ .../srcds/templates/dodgeball_presets.cfg.j2 | 56 + roles/srcds/templates/motd.txt.j2 | 9 +- shell.nix | 7 +- sm_plugins/TF2-Dodgeball-Modified | 1 + sm_plugins_update.sh | 25 + 33 files changed, 9395 insertions(+), 377 deletions(-) delete mode 100644 Makefile create mode 100644 justfile create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_utilities.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/multicolors.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_airblast_prevention.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_ffa.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_no_block.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_print.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_speedhud.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/thirdperson.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt create mode 100644 roles/sourcemod/files/cfg/sourcemod/dodgeball_disable.cfg create mode 100644 roles/sourcemod/files/cfg/sourcemod/dodgeball_enable.cfg create mode 100644 roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_disable.cfg create mode 100644 roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_enable.cfg create mode 100644 roles/srcds/templates/dodgeball_general.cfg.j2 create mode 100644 roles/srcds/templates/dodgeball_presets.cfg.j2 create mode 160000 sm_plugins/TF2-Dodgeball-Modified diff --git a/.gitmodules b/.gitmodules index abc8dab2..ea703b85 100644 --- a/.gitmodules +++ b/.gitmodules @@ -47,3 +47,6 @@ [submodule "sm_plugins/HalloweenCosmeticEnabler"] path = sm_plugins/HalloweenCosmeticEnabler url = https://github.com/Mikusch/HalloweenCosmeticEnabler +[submodule "sm_plugins/TF2-Dodgeball-Modified"] + path = sm_plugins/TF2-Dodgeball-Modified + url = https://github.com/Silorak/TF2-Dodgeball-Modified.git diff --git a/Makefile b/Makefile deleted file mode 100644 index 58ceedd7..00000000 --- a/Makefile +++ /dev/null @@ -1,68 +0,0 @@ -.PHONY: all pre system deploy - -PLAYBOOK_PATH := ./ -HOSTS := hosts.yml -USER := tf2server -FORKS := 20 -HOSTS_DEV := dev.hosts -HOSTS_PROD := prod.hosts - -all: site - -#format: -# @find ./roles/sourcemod/files/addons/sourcemod/scripting -regex '.*\.\(sp\)' -exec clang-format -style=file -i {} \; - -lint: - @ansible-lint --exclude sm_plugins watcher - -deps: - @ansible-galaxy collection install -r collections/requirements.yml - -adduser: - @ansible-playbook -u root -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/adduser.yml - -srcds: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/srcds.yml - -web: - ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/web.yml --limit metrics - -vpn: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/vpn.yml - -system: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/system.yml - -site: - ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) site.yml - -ping: - @ansible tf2 -m ping $(ARGS) - -update: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/update.yml --limit sao-1.br.uncletopia.com - -game_engine: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) --tags game_engine $(PLAYBOOK_PATH)/srcds.yml - -game_config: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) --tags game_config $(PLAYBOOK_PATH)/srcds.yml - -uncledane: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/uncledane.yml - -bdapi: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/bdapi.yml - -demostats: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/demostats.yml - -tf2bdd: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/tf2bdd.yml - -sentry: - @ansible-playbook -u $(USER) -i $(HOSTS_PROD) --forks $(FORKS) $(PLAYBOOK_PATH)/sentry.yml - -srcds-test: - @ansible-playbook -u $(USER) -i $(HOSTS_DEV) --forks $(FORKS) $(PLAYBOOK_PATH)/srcds.yml - diff --git a/justfile b/justfile new file mode 100644 index 00000000..e66322c3 --- /dev/null +++ b/justfile @@ -0,0 +1,64 @@ +PLAYBOOK_PATH := "./" +HOSTS := "hosts.yml" +USER := "tf2server" +FORKS := "20" +inventory := "prod.hosts" + +all: site + +#format: +# @find ./roles/sourcemod/files/addons/sourcemod/scripting -regex '.*\.\(sp\)' -exec clang-format -style=file -i {} \; + +lint: + @ansible-lint --exclude sm_plugins watcher + +deps: + @ansible-galaxy collection install -r collections/requirements.yml + +adduser: + @ansible-playbook -u root -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/adduser.yml + +srcds: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/srcds.yml + +web: + ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/web.yml --limit metrics + +vpn: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/vpn.yml + +system: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/system.yml + +site: + ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) site.yml + +ping: + @ansible tf2 -m ping $(ARGS) + +update: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/update.yml + +game_engine: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) --tags game_engine $(PLAYBOOK_PATH)/srcds.yml + +game_config: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) --tags game_config $(PLAYBOOK_PATH)/srcds.yml + +uncledane: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/uncledane.yml + +bdapi: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/bdapi.yml + +demostats: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/demostats.yml + +tf2bdd: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/tf2bdd.yml + +sentry: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/sentry.yml + +srcds-test: + @ansible-playbook -u $(USER) -i $(inventory) --forks $(FORKS) $(PLAYBOOK_PATH)/srcds.yml diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp b/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp new file mode 100644 index 00000000..e1575846 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp @@ -0,0 +1,580 @@ +// ********************************************************************************* +// PREPROCESSOR +// ********************************************************************************* +#pragma semicolon 1 +#pragma newdecls required + +// ********************************************************************************* +// INCLUDES +// ********************************************************************************* +#include +#include +#include +#include + +#include + +// ********************************************************************************* +// CONSTANTS +// ********************************************************************************* +#define PLUGIN_NAME "[TF2] Dodgeball" +#define PLUGIN_AUTHOR "Damizean, x07x08 continued by Silorak" +#define PLUGIN_VERSION "2.1.0" +#define PLUGIN_CONTACT "https://github.com/Silorak/TF2-Dodgeball-Modified" + +enum Musics +{ + Music_RoundStart, + Music_RoundWin, + Music_RoundLose, + Music_Gameplay, + SizeOfMusicsArray +} + +// ********************************************************************************* +// VARIABLES +// ********************************************************************************* + +// -----<<< Cvars >>>----- +ConVar CvarEnabled; +ConVar CvarEnableCfgFile; +ConVar CvarDisableCfgFile; +ConVar CvarStealPreventionNumber; +ConVar CvarStealPreventionDamage; +ConVar CvarStealDistance; +ConVar CvarDelayPrevention; +ConVar CvarDelayPreventionTime; +ConVar CvarDelayPreventionSpeedup; +ConVar CvarNoTargetRedirectDamage; +ConVar CvarStealMessage; +ConVar CvarDelayMessage; + + + +// -----<<< Gameplay >>>----- +bool Enabled; +bool RoundStarted; +int RoundCount; +int RocketsFired; +Handle LogicTimer; +float NextSpawnTime; +int LastDeadTeam; +int LastDeadClient; +int PlayerCount; +int LastStealer; + +// Cached SendProp offset for rocket damage (m_iDeflected + 4). +// Looked up once at plugin start instead of calling FindSendPropInfo every frame. +int DamageOffset; + +eRocketSteal StealInfo[MAXPLAYERS + 1]; + +// -----<<< Configuration >>>----- +bool MusicEnabled; +bool UseOrbitCoefficient; +bool UseTargetSpeedScaling; +bool UseSmoothElevation; +bool UseBounceVerticalScale; +bool Music[view_as(SizeOfMusicsArray)]; +char MusicPath[view_as(SizeOfMusicsArray)][PLATFORM_MAX_PATH]; +bool UseWebPlayer; +char WebPlayerUrl[256]; + +// -----<<< Structures >>>----- +// Rockets +bool RocketIsValid[MAX_ROCKETS]; +int RocketEntity[MAX_ROCKETS]; +int RocketTarget[MAX_ROCKETS]; +int RocketClass[MAX_ROCKETS]; +RocketFlags RocketInstanceFlags[MAX_ROCKETS]; +RocketState RocketInstanceState[MAX_ROCKETS]; +float RocketSpeed[MAX_ROCKETS]; +float RocketMphSpeed[MAX_ROCKETS]; +float RocketDirection[MAX_ROCKETS][3]; +int RocketDeflections[MAX_ROCKETS]; +int RocketEventDeflections[MAX_ROCKETS]; +float RocketLastDeflectionTime[MAX_ROCKETS]; +float RocketLastBeepTime[MAX_ROCKETS]; +float LastSpawnTime[MAX_ROCKETS]; +int RocketBounces[MAX_ROCKETS]; +bool RocketHomingPaused[MAX_ROCKETS]; +bool RocketIsDragPause[MAX_ROCKETS]; // true = drag pause (per-frame unpause), false = bounce pause (timer unpause) +float RocketDragPauseEnd[MAX_ROCKETS]; // GetGameTime() when drag pause should end +int RocketCount; + +// Classes +char RocketClassName[MAX_ROCKET_CLASSES][16]; +char RocketClassLongName[MAX_ROCKET_CLASSES][32]; +BehaviourTypes RocketClassBehaviour[MAX_ROCKET_CLASSES]; +char RocketClassModel[MAX_ROCKET_CLASSES][PLATFORM_MAX_PATH]; +RocketFlags RocketClassFlags[MAX_ROCKET_CLASSES]; +float RocketClassBeepInterval[MAX_ROCKET_CLASSES]; +char RocketClassSpawnSound[MAX_ROCKET_CLASSES][PLATFORM_MAX_PATH]; +char RocketClassBeepSound[MAX_ROCKET_CLASSES][PLATFORM_MAX_PATH]; +char RocketClassAlertSound[MAX_ROCKET_CLASSES][PLATFORM_MAX_PATH]; +float RocketClassCritChance[MAX_ROCKET_CLASSES]; +float RocketClassDamage[MAX_ROCKET_CLASSES]; +float RocketClassDamageIncrement[MAX_ROCKET_CLASSES]; +float RocketClassSpeed[MAX_ROCKET_CLASSES]; +float RocketClassSpeedIncrement[MAX_ROCKET_CLASSES]; +float RocketClassSpeedLimit[MAX_ROCKET_CLASSES]; +float RocketClassTurnRate[MAX_ROCKET_CLASSES]; +float RocketClassTurnRateIncrement[MAX_ROCKET_CLASSES]; +float RocketClassTurnRateLimit[MAX_ROCKET_CLASSES]; +float RocketClassElevationRate[MAX_ROCKET_CLASSES]; +float RocketClassElevationLimit[MAX_ROCKET_CLASSES]; +float RocketClassRocketsModifier[MAX_ROCKET_CLASSES]; +float RocketClassPlayerModifier[MAX_ROCKET_CLASSES]; +float RocketClassControlDelay[MAX_ROCKET_CLASSES]; +float RocketClassTargetWeight[MAX_ROCKET_CLASSES]; +DataPack RocketClassCmdsOnSpawn[MAX_ROCKET_CLASSES]; +DataPack RocketClassCmdsOnDeflect[MAX_ROCKET_CLASSES]; +DataPack RocketClassCmdsOnKill[MAX_ROCKET_CLASSES]; +DataPack RocketClassCmdsOnExplode[MAX_ROCKET_CLASSES]; +DataPack RocketClassCmdsOnNoTarget[MAX_ROCKET_CLASSES]; +int RocketClassMaxBounces[MAX_ROCKET_CLASSES]; +float RocketClassBounceScale[MAX_ROCKET_CLASSES]; +float RocketClassOrbitTightness[MAX_ROCKET_CLASSES]; +float RocketClassMaxSpeed[MAX_ROCKET_CLASSES]; +int RocketClassMaxDeflections[MAX_ROCKET_CLASSES]; +float RocketClassBounceVerticalScale[MAX_ROCKET_CLASSES]; +float RocketClassBounceMaxVerticalSpeed[MAX_ROCKET_CLASSES]; +float RocketClassDragPauseDuration[MAX_ROCKET_CLASSES]; +int RocketClassCount; + +// Spawner classes +char SpawnersName[MAX_SPAWNER_CLASSES][32]; +int SpawnersMaxRockets[MAX_SPAWNER_CLASSES]; +float SpawnersInterval[MAX_SPAWNER_CLASSES]; +ArrayList SpawnersChancesTable[MAX_SPAWNER_CLASSES]; +StringMap SpawnersTrie; +int SpawnersCount; + +int CurrentRedSpawn; +int SpawnPointsRedCount; +int SpawnPointsRedClass[MAX_SPAWN_POINTS]; +int SpawnPointsRedEntity[MAX_SPAWN_POINTS]; + +int CurrentBluSpawn; +int SpawnPointsBluCount; +int SpawnPointsBluClass[MAX_SPAWN_POINTS]; +int SpawnPointsBluEntity[MAX_SPAWN_POINTS]; + +int DefaultRedSpawner; +int DefaultBluSpawner; + +// -----<<< Presets >>>----- +char PresetName[MAX_PRESETS][64]; +char PresetRocketClass[MAX_PRESETS][16]; +int PresetMaxRockets[MAX_PRESETS]; +float PresetSpawnInterval[MAX_PRESETS]; +int PresetCount; + +// -----<<< Forward handles >>>----- +Handle ForwardOnRocketCreated; +Handle ForwardOnRocketCreatedPre; +Handle ForwardOnRocketDeflect; +Handle ForwardOnRocketDeflectPre; +Handle ForwardOnRocketSteal; +Handle ForwardOnRocketNoTarget; +Handle ForwardOnRocketDelay; +Handle ForwardOnRocketBounce; +Handle ForwardOnRocketBouncePre; +Handle ForwardOnRocketsConfigExecuted; +Handle ForwardOnRocketStateChanged; + +// ********************************************************************************* +// PLUGIN LOGIC (INCLUDES) +// ********************************************************************************* +#include "dodgeball_utilities.inc" +#include "dodgeball_config.inc" +#include "dodgeball_rockets.inc" +#include "dodgeball_events.inc" +#include "dodgeball_core.inc" +#include "dodgeball_natives.inc" + +// ********************************************************************************* +// PLUGIN INFO & LIFECYCLE +// ********************************************************************************* +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_NAME, + version = PLUGIN_VERSION, + url = PLUGIN_CONTACT +}; + +public void OnPluginStart() +{ + char modName[32]; GetGameFolderName(modName, sizeof(modName)); + if (!StrEqual(modName, "tf")) SetFailState("This plugin is only for Team Fortress 2."); + + LoadTranslations("tfdb.phrases.txt"); + + CreateConVar("tf_dodgeball_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_SPONLY|FCVAR_DONTRECORD); + CvarEnabled = CreateConVar("tf_dodgeball_enabled", "1", "Enable Dodgeball on TFDB maps?", _, true, 0.0, true, 1.0); + CvarEnableCfgFile = CreateConVar("tf_dodgeball_enablecfg", "sourcemod/dodgeball_enable.cfg", "Config file to execute when enabling the Dodgeball game mode."); + CvarDisableCfgFile = CreateConVar("tf_dodgeball_disablecfg", "sourcemod/dodgeball_disable.cfg", "Config file to execute when disabling the Dodgeball game mode."); + CvarStealPreventionNumber = CreateConVar("tf_dodgeball_sp_number", "3", "How many steals before you get slayed?", _, true, 0.0, false); + CvarStealPreventionDamage = CreateConVar("tf_dodgeball_sp_damage", "0", "Reduce all damage on stolen rockets?", _, true, 0.0, true, 1.0); + CvarStealDistance = CreateConVar("tf_dodgeball_sp_distance", "48.0", "The distance between players for a steal to register.", _, true, 0.0, false); + CvarDelayPrevention = CreateConVar("tf_dodgeball_delay_prevention", "1", "Enable delay prevention?", _, true, 0.0, true, 1.0); + CvarDelayPreventionTime = CreateConVar("tf_dodgeball_dp_time", "5", "How much time (in seconds) before delay prevention activates?", _, true, 0.0, false); + CvarDelayPreventionSpeedup = CreateConVar("tf_dodgeball_dp_speedup", "100", "How much speed (in hammer units per second) should the rocket gain when delayed?", _, true, 0.0, false); + CvarNoTargetRedirectDamage = CreateConVar("tf_dodgeball_redirect_damage", "1", "Reduce all damage when a rocket has an invalid target?", _, true, 0.0, true, 1.0); + CvarStealMessage = CreateConVar("tf_dodgeball_sp_message", "1", "Display the steal message(s)?", _, true, 0.0, true, 1.0); + CvarDelayMessage = CreateConVar("tf_dodgeball_dp_message", "1", "Display the delay message(s)?", _, true, 0.0, true, 1.0); + + + + SpawnersTrie = new StringMap(); + + // Cache the SendProp offset for rocket damage once at plugin start. + // This is m_iDeflected + 4 bytes, used to set rocket damage via SetEntDataFloat. + DamageOffset = FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4; + + AddTempEntHook("TFExplosion", OnTFExplosion); + + RegisterCommands(); +} + +public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] strError, int iErrMax) +{ + CreateNative("TFDB_IsValidRocket", Native_IsValidRocket); + CreateNative("TFDB_FindRocketByEntity", Native_FindRocketByEntity); + CreateNative("TFDB_IsDodgeballEnabled", Native_IsDodgeballEnabled); + CreateNative("TFDB_GetRocketEntity", Native_GetRocketEntity); + CreateNative("TFDB_GetRocketFlags", Native_GetRocketFlags); + CreateNative("TFDB_SetRocketFlags", Native_SetRocketFlags); + CreateNative("TFDB_GetRocketTarget", Native_GetRocketTarget); + CreateNative("TFDB_SetRocketTarget", Native_SetRocketTarget); + CreateNative("TFDB_GetRocketEventDeflections", Native_GetRocketEventDeflections); + CreateNative("TFDB_SetRocketEventDeflections", Native_SetRocketEventDeflections); + CreateNative("TFDB_GetRocketDeflections", Native_GetRocketDeflections); + CreateNative("TFDB_SetRocketDeflections", Native_SetRocketDeflections); + CreateNative("TFDB_GetRocketClass", Native_GetRocketClass); + CreateNative("TFDB_SetRocketClass", Native_SetRocketClass); + CreateNative("TFDB_GetRocketClassCount", Native_GetRocketClassCount); + CreateNative("TFDB_GetRocketClassBehaviour", Native_GetRocketClassBehaviour); + CreateNative("TFDB_SetRocketClassBehaviour", Native_SetRocketClassBehaviour); + CreateNative("TFDB_GetRocketClassFlags", Native_GetRocketClassFlags); + CreateNative("TFDB_SetRocketClassFlags", Native_SetRocketClassFlags); + CreateNative("TFDB_GetRocketClassDamage", Native_GetRocketClassDamage); + CreateNative("TFDB_SetRocketClassDamage", Native_SetRocketClassDamage); + CreateNative("TFDB_GetRocketClassDamageIncrement", Native_GetRocketClassDamageIncrement); + CreateNative("TFDB_SetRocketClassDamageIncrement", Native_SetRocketClassDamageIncrement); + CreateNative("TFDB_GetRocketClassSpeed", Native_GetRocketClassSpeed); + CreateNative("TFDB_SetRocketClassSpeed", Native_SetRocketClassSpeed); + CreateNative("TFDB_GetRocketClassSpeedIncrement", Native_GetRocketClassSpeedIncrement); + CreateNative("TFDB_SetRocketClassSpeedIncrement", Native_SetRocketClassSpeedIncrement); + CreateNative("TFDB_GetRocketClassSpeedLimit", Native_GetRocketClassSpeedLimit); + CreateNative("TFDB_SetRocketClassSpeedLimit", Native_SetRocketClassSpeedLimit); + CreateNative("TFDB_GetRocketClassTurnRate", Native_GetRocketClassTurnRate); + CreateNative("TFDB_SetRocketClassTurnRate", Native_SetRocketClassTurnRate); + CreateNative("TFDB_GetRocketClassTurnRateIncrement", Native_GetRocketClassTurnRateIncrement); + CreateNative("TFDB_SetRocketClassTurnRateIncrement", Native_SetRocketClassTurnRateIncrement); + CreateNative("TFDB_GetRocketClassTurnRateLimit", Native_GetRocketClassTurnRateLimit); + CreateNative("TFDB_SetRocketClassTurnRateLimit", Native_SetRocketClassTurnRateLimit); + CreateNative("TFDB_GetRocketClassElevationRate", Native_GetRocketClassElevationRate); + CreateNative("TFDB_SetRocketClassElevationRate", Native_SetRocketClassElevationRate); + CreateNative("TFDB_GetRocketClassElevationLimit", Native_GetRocketClassElevationLimit); + CreateNative("TFDB_SetRocketClassElevationLimit", Native_SetRocketClassElevationLimit); + CreateNative("TFDB_GetRocketClassRocketsModifier", Native_GetRocketClassRocketsModifier); + CreateNative("TFDB_SetRocketClassRocketsModifier", Native_SetRocketClassRocketsModifier); + CreateNative("TFDB_GetRocketClassPlayerModifier", Native_GetRocketClassPlayerModifier); + CreateNative("TFDB_SetRocketClassPlayerModifier", Native_SetRocketClassPlayerModifier); + CreateNative("TFDB_GetRocketClassControlDelay", Native_GetRocketClassControlDelay); + CreateNative("TFDB_SetRocketClassControlDelay", Native_SetRocketClassControlDelay); + + CreateNative("TFDB_SetRocketClassCount", Native_SetRocketClassCount); + CreateNative("TFDB_SetRocketEntity", Native_SetRocketEntity); + CreateNative("TFDB_GetRocketClassMaxBounces", Native_GetRocketClassMaxBounces); + CreateNative("TFDB_SetRocketClassMaxBounces", Native_SetRocketClassMaxBounces); + CreateNative("TFDB_GetSpawnersName", Native_GetSpawnersName); + CreateNative("TFDB_SetSpawnersName", Native_SetSpawnersName); + CreateNative("TFDB_GetSpawnersMaxRockets", Native_GetSpawnersMaxRockets); + CreateNative("TFDB_SetSpawnersMaxRockets", Native_SetSpawnersMaxRockets); + CreateNative("TFDB_GetSpawnersInterval", Native_GetSpawnersInterval); + CreateNative("TFDB_SetSpawnersInterval", Native_SetSpawnersInterval); + CreateNative("TFDB_GetSpawnersChancesTable", Native_GetSpawnersChancesTable); + CreateNative("TFDB_SetSpawnersChancesTable", Native_SetSpawnersChancesTable); + CreateNative("TFDB_GetSpawnersCount", Native_GetSpawnersCount); + CreateNative("TFDB_SetSpawnersCount", Native_SetSpawnersCount); + CreateNative("TFDB_GetCurrentRedSpawn", Native_GetCurrentRedSpawn); + CreateNative("TFDB_SetCurrentRedSpawn", Native_SetCurrentRedSpawn); + CreateNative("TFDB_GetSpawnPointsRedCount", Native_GetSpawnPointsRedCount); + CreateNative("TFDB_SetSpawnPointsRedCount", Native_SetSpawnPointsRedCount); + CreateNative("TFDB_GetSpawnPointsRedClass", Native_GetSpawnPointsRedClass); + CreateNative("TFDB_SetSpawnPointsRedClass", Native_SetSpawnPointsRedClass); + CreateNative("TFDB_GetSpawnPointsRedEntity", Native_GetSpawnPointsRedEntity); + CreateNative("TFDB_SetSpawnPointsRedEntity", Native_SetSpawnPointsRedEntity); + CreateNative("TFDB_GetCurrentBluSpawn", Native_GetCurrentBluSpawn); + CreateNative("TFDB_SetCurrentBluSpawn", Native_SetCurrentBluSpawn); + CreateNative("TFDB_GetSpawnPointsBluCount", Native_GetSpawnPointsBluCount); + CreateNative("TFDB_SetSpawnPointsBluCount", Native_SetSpawnPointsBluCount); + CreateNative("TFDB_GetSpawnPointsBluClass", Native_GetSpawnPointsBluClass); + CreateNative("TFDB_SetSpawnPointsBluClass", Native_SetSpawnPointsBluClass); + CreateNative("TFDB_GetSpawnPointsBluEntity", Native_GetSpawnPointsBluEntity); + CreateNative("TFDB_SetSpawnPointsBluEntity", Native_SetSpawnPointsBluEntity); + CreateNative("TFDB_GetRoundStarted", Native_GetRoundStarted); + CreateNative("TFDB_GetRoundCount", Native_GetRoundCount); + CreateNative("TFDB_GetRocketsFired", Native_GetRocketsFired); + CreateNative("TFDB_GetNextSpawnTime", Native_GetNextSpawnTime); + CreateNative("TFDB_SetNextSpawnTime", Native_SetNextSpawnTime); + CreateNative("TFDB_GetLastDeadTeam", Native_GetLastDeadTeam); + CreateNative("TFDB_GetLastDeadClient", Native_GetLastDeadClient); + CreateNative("TFDB_GetLastStealer", Native_GetLastStealer); + CreateNative("TFDB_GetRocketSpeed", Native_GetRocketSpeed); + CreateNative("TFDB_SetRocketSpeed", Native_SetRocketSpeed); + CreateNative("TFDB_GetRocketMphSpeed", Native_GetRocketMphSpeed); + CreateNative("TFDB_SetRocketMphSpeed", Native_SetRocketMphSpeed); + CreateNative("TFDB_GetRocketDirection", Native_GetRocketDirection); + CreateNative("TFDB_SetRocketDirection", Native_SetRocketDirection); + CreateNative("TFDB_GetRocketLastDeflectionTime", Native_GetRocketLastDeflectionTime); + CreateNative("TFDB_SetRocketLastDeflectionTime", Native_SetRocketLastDeflectionTime); + CreateNative("TFDB_GetRocketLastBeepTime", Native_GetRocketLastBeepTime); + CreateNative("TFDB_SetRocketLastBeepTime", Native_SetRocketLastBeepTime); + CreateNative("TFDB_GetRocketCount", Native_GetRocketCount); + CreateNative("TFDB_GetLastSpawnTime", Native_GetLastSpawnTime); + CreateNative("TFDB_GetRocketBounces", Native_GetRocketBounces); + CreateNative("TFDB_SetRocketBounces", Native_SetRocketBounces); + CreateNative("TFDB_GetRocketClassName", Native_GetRocketClassName); + CreateNative("TFDB_SetRocketClassName", Native_SetRocketClassName); + CreateNative("TFDB_GetRocketClassLongName", Native_GetRocketClassLongName); + CreateNative("TFDB_SetRocketClassLongName", Native_SetRocketClassLongName); + CreateNative("TFDB_GetRocketClassModel", Native_GetRocketClassModel); + CreateNative("TFDB_SetRocketClassModel", Native_SetRocketClassModel); + CreateNative("TFDB_GetRocketClassBeepInterval", Native_GetRocketClassBeepInterval); + CreateNative("TFDB_SetRocketClassBeepInterval", Native_SetRocketClassBeepInterval); + CreateNative("TFDB_GetRocketClassSpawnSound", Native_GetRocketClassSpawnSound); + CreateNative("TFDB_SetRocketClassSpawnSound", Native_SetRocketClassSpawnSound); + CreateNative("TFDB_GetRocketClassBeepSound", Native_GetRocketClassBeepSound); + CreateNative("TFDB_SetRocketClassBeepSound", Native_SetRocketClassBeepSound); + CreateNative("TFDB_GetRocketClassAlertSound", Native_GetRocketClassAlertSound); + CreateNative("TFDB_SetRocketClassAlertSound", Native_SetRocketClassAlertSound); + CreateNative("TFDB_GetRocketClassCritChance", Native_GetRocketClassCritChance); + CreateNative("TFDB_SetRocketClassCritChance", Native_SetRocketClassCritChance); + CreateNative("TFDB_GetRocketClassTargetWeight", Native_GetRocketClassTargetWeight); + CreateNative("TFDB_SetRocketClassTargetWeight", Native_SetRocketClassTargetWeight); + CreateNative("TFDB_GetRocketClassCmdsOnSpawn", Native_GetRocketClassCmdsOnSpawn); + CreateNative("TFDB_SetRocketClassCmdsOnSpawn", Native_SetRocketClassCmdsOnSpawn); + CreateNative("TFDB_GetRocketClassCmdsOnDeflect", Native_GetRocketClassCmdsOnDeflect); + CreateNative("TFDB_SetRocketClassCmdsOnDeflect", Native_SetRocketClassCmdsOnDeflect); + CreateNative("TFDB_GetRocketClassCmdsOnKill", Native_GetRocketClassCmdsOnKill); + CreateNative("TFDB_SetRocketClassCmdsOnKill", Native_SetRocketClassCmdsOnKill); + CreateNative("TFDB_GetRocketClassCmdsOnExplode", Native_GetRocketClassCmdsOnExplode); + CreateNative("TFDB_SetRocketClassCmdsOnExplode", Native_SetRocketClassCmdsOnExplode); + CreateNative("TFDB_GetRocketClassCmdsOnNoTarget", Native_GetRocketClassCmdsOnNoTarget); + CreateNative("TFDB_SetRocketClassCmdsOnNoTarget", Native_SetRocketClassCmdsOnNoTarget); + CreateNative("TFDB_GetRocketClassBounceScale", Native_GetRocketClassBounceScale); + CreateNative("TFDB_SetRocketClassBounceScale", Native_SetRocketClassBounceScale); + CreateNative("TFDB_CreateRocket", Native_CreateRocket); + CreateNative("TFDB_DestroyRocket", Native_DestroyRocket); + CreateNative("TFDB_DestroyRockets", Native_DestroyRockets); + CreateNative("TFDB_DestroyRocketClasses", Native_DestroyRocketClasses); + CreateNative("TFDB_DestroySpawners", Native_DestroySpawners); + CreateNative("TFDB_ParseConfigurations", Native_ParseConfigurations); + CreateNative("TFDB_PopulateSpawnPoints", Native_PopulateSpawnPoints); + CreateNative("TFDB_HomingRocketThink", Native_HomingRocketThink); + CreateNative("TFDB_RocketLegacyThink", Native_RocketLegacyThink); + CreateNative("TFDB_GetRocketState", Native_GetRocketState); + CreateNative("TFDB_SetRocketState", Native_SetRocketState); + CreateNative("TFDB_GetStealInfo", Native_GetStealInfo); + CreateNative("TFDB_SetStealInfo", Native_SetStealInfo); + CreateNative("TFDB_GetPresetCount", Native_GetPresetCount); + CreateNative("TFDB_GetPresetName", Native_GetPresetName); + CreateNative("TFDB_ApplyPreset", Native_ApplyPreset); + + SetupForwards(); + + RegPluginLibrary("tfdb"); + + return APLRes_Success; +} + +void SetupForwards() +{ + ForwardOnRocketCreated = CreateGlobalForward("TFDB_OnRocketCreated", ET_Ignore, Param_Cell, Param_Cell); + ForwardOnRocketCreatedPre = CreateGlobalForward("TFDB_OnRocketCreatedPre", ET_Event, Param_Cell, Param_CellByRef, Param_CellByRef); + ForwardOnRocketDeflect = CreateGlobalForward("TFDB_OnRocketDeflect", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + ForwardOnRocketDeflectPre = CreateGlobalForward("TFDB_OnRocketDeflectPre", ET_Event, Param_Cell, Param_Cell, Param_Cell, Param_CellByRef); + ForwardOnRocketSteal = CreateGlobalForward("TFDB_OnRocketSteal", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell); + ForwardOnRocketNoTarget = CreateGlobalForward("TFDB_OnRocketNoTarget", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + ForwardOnRocketDelay = CreateGlobalForward("TFDB_OnRocketDelay", ET_Ignore, Param_Cell, Param_Cell); + ForwardOnRocketBounce = CreateGlobalForward("TFDB_OnRocketBounce", ET_Ignore, Param_Cell, Param_Cell); + ForwardOnRocketBouncePre = CreateGlobalForward("TFDB_OnRocketBouncePre", ET_Event, Param_Cell, Param_Cell, Param_Array, Param_Array); + ForwardOnRocketsConfigExecuted = CreateGlobalForward("TFDB_OnRocketsConfigExecuted", ET_Ignore, Param_String); + ForwardOnRocketStateChanged = CreateGlobalForward("TFDB_OnRocketStateChanged", ET_Ignore, Param_Cell, Param_Cell, Param_Cell); +} + +public void OnConfigsExecuted() +{ + if (!(CvarEnabled.BoolValue && IsDodgeBallMap())) return; + + EnableDodgeBall(); +} + +public void OnMapEnd() +{ + DisableDodgeBall(); +} + +void Forward_OnRocketCreated(int index, int entity) +{ + Call_StartForward(ForwardOnRocketCreated); + Call_PushCell(index); + Call_PushCell(entity); + Call_Finish(); +} + +Action Forward_OnRocketCreatedPre(int index, int &rocketClass, RocketFlags &flags) +{ + Action result; + + Call_StartForward(ForwardOnRocketCreatedPre); + Call_PushCell(index); + Call_PushCellRef(rocketClass); + Call_PushCellRef(flags); + Call_Finish(result); + + return result; +} + +void Forward_OnRocketDeflect(int index, int entity, int owner) +{ + Call_StartForward(ForwardOnRocketDeflect); + Call_PushCell(index); + Call_PushCell(entity); + Call_PushCell(owner); + Call_Finish(); +} + +Action Forward_OnRocketDeflectPre(int index, int entity, int owner, int &target) +{ + Action result; + + Call_StartForward(ForwardOnRocketDeflectPre); + Call_PushCell(index); + Call_PushCell(entity); + Call_PushCell(owner); + Call_PushCellRef(target); + Call_Finish(result); + + return result; +} + +void Forward_OnRocketSteal(int index, int owner, int target, int stealCount) +{ + Call_StartForward(ForwardOnRocketSteal); + Call_PushCell(index); + Call_PushCell(owner); + Call_PushCell(target); + Call_PushCell(stealCount); + Call_Finish(); +} + +void Forward_OnRocketNoTarget(int index, int target, int owner) +{ + Call_StartForward(ForwardOnRocketNoTarget); + Call_PushCell(index); + Call_PushCell(target); + Call_PushCell(owner); + Call_Finish(); +} + +void Forward_OnRocketDelay(int index, int target) +{ + Call_StartForward(ForwardOnRocketDelay); + Call_PushCell(index); + Call_PushCell(target); + Call_Finish(); +} + +void Forward_OnRocketBounce(int index, int entity) +{ + Call_StartForward(ForwardOnRocketBounce); + Call_PushCell(index); + Call_PushCell(entity); + Call_Finish(); +} + +Action Forward_OnRocketBouncePre(int index, int entity, float angles[3], float velocity[3]) +{ + Action result; + + Call_StartForward(ForwardOnRocketBouncePre); + Call_PushCell(index); + Call_PushCell(entity); + Call_PushArrayEx(angles, sizeof(angles), SM_PARAM_COPYBACK); + Call_PushArrayEx(velocity, sizeof(velocity), SM_PARAM_COPYBACK); + Call_Finish(result); + + return result; +} + +void Forward_OnRocketsConfigExecuted(const char[] configFile) +{ + Call_StartForward(ForwardOnRocketsConfigExecuted); + Call_PushString(configFile); + Call_Finish(); +} + +void Forward_OnRocketStateChanged(int index, RocketState state, RocketState newState) +{ + Call_StartForward(ForwardOnRocketStateChanged); + Call_PushCell(index); + Call_PushCell(state); + Call_PushCell(newState); + Call_Finish(); +} + +void Internal_SetRocketState(int index, RocketState newState) +{ + RocketState state = RocketInstanceState[index]; + if (state == newState) + { + return; + } + + RocketInstanceState[index] = newState; + Forward_OnRocketStateChanged(index, state, newState); +} + +public bool MLTargetFilterStealer(const char[] pattern, ArrayList clients) +{ + bool reverse = (StrContains(pattern, "!", false) == 1); + for (int client = 1; client <= MaxClients; client++) + { + if (!(IsClientInGame(client) && (clients.FindValue(client) == -1))) continue; + + if (client == LastStealer) + { + if (!reverse) + { + clients.Push(client); + } + } + else if (reverse) + { + clients.Push(client); + } + } + + return !!clients.Length; +} + +#if defined _multicolors_included && defined _more_colors_included && defined _colors_included +stock void CSkipNextClient(int client) +{ + if (!IsSource2009()) + { + C_SkipNextClient(client); + } + else + { + MC_SkipNextClient(client); + } +} + +#endif diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/colors.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/colors.inc index 45fd1ef2..170ec97f 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/colors.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/colors.inc @@ -1,10 +1,11 @@ /************************************************************************** * * * Colored Chat Functions * - * Author: exvel * - * Version: 1.0.5 * + * Author: exvel, Editor: Popoklopsi, Powerlord, Bara * + * Version: 2.0.0-MC * * * **************************************************************************/ + #if defined _colors_included #endinput @@ -12,33 +13,47 @@ #define _colors_included #define MAX_MESSAGE_LENGTH 250 -#define MAX_COLORS 6 +#define MAX_COLORS 18 #define SERVER_INDEX 0 #define NO_INDEX -1 #define NO_PLAYER -2 -enum Colors -{ +enum C_Colors { Color_Default = 0, + Color_Darkred, Color_Green, Color_Lightgreen, Color_Red, Color_Blue, - Color_Olive + Color_Olive, + Color_Lime, + Color_Lightred, + Color_Purple, + Color_Grey, + Color_Yellow, + Color_Orange, + Color_Bluegrey, + Color_Lightblue, + Color_Darkblue, + Color_Grey2, + Color_Orchid, + Color_Lightred2 } -/* Colors' properties */ -new String:CTag[][] = {"{default}", "{green}", "{lightgreen}", "{red}", "{blue}", "{olive}"}; -new String:CTagCode[][] = {"\x01", "\x04", "\x03", "\x03", "\x03", "\x05"}; -new bool:CTagReqSayText2[] = {false, false, true, true, true, false}; -new bool:CEventIsHooked = false; -new bool:CSkipList[MAXPLAYERS+1] = {false,...}; +/* C_Colors' properties */ +char C_Tag[][] = {"{default}", "{darkred}", "{green}", "{lightgreen}", "{red}", "{blue}", "{olive}", "{lime}", "{lightred}", "{purple}", "{grey}", "{yellow}", "{orange}", "{bluegrey}", "{lightblue}", "{darkblue}", "{grey2}", "{orchid}", "{lightred2}"}; +char C_TagCode[][] = {"\x01", "\x02", "\x04", "\x03", "\x03", "\x03", "\x05", "\x06", "\x07", "\x03", "\x08", "\x09", "\x10", "\x0A", "\x0B", "\x0C", "\x0D", "\x0E", "\x0F"}; +bool C_TagReqSayText2[] = {false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false}; +bool C_EventIsHooked; +bool C_SkipList[MAXPLAYERS+1]; /* Game default profile */ -new bool:CProfile_Colors[] = {true, true, false, false, false, false}; -new CProfile_TeamIndex[] = {NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX}; -new bool:CProfile_SayText2 = false; +bool C_Profile_Colors[] = {true, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; +int C_Profile_TeamIndex[] = {NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX}; +bool C_Profile_SayText2; + +static ConVar sm_show_activity; /** * Prints a message to a specific client in the chat area. @@ -50,28 +65,91 @@ new bool:CProfile_SayText2 = false; * * On error/Errors: If the client is not connected an error will be thrown. */ -stock CPrintToChat(client, const String:szMessage[], any:...) -{ - if (client <= 0 || client > MaxClients) +stock void C_PrintToChat(int client, const char[] szMessage, any ...) { + if (client <= 0 || client > MaxClients) { ThrowError("Invalid client index %d", client); - - if (!IsClientInGame(client)) + } + + if (!IsClientInGame(client)) { ThrowError("Client %d is not in game", client); - - decl String:szBuffer[MAX_MESSAGE_LENGTH]; - decl String:szCMessage[MAX_MESSAGE_LENGTH]; + } + + char szBuffer[MAX_MESSAGE_LENGTH]; + char szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage); VFormat(szCMessage, sizeof(szCMessage), szBuffer, 3); - - new index = CFormat(szCMessage, sizeof(szCMessage)); - if (index == NO_INDEX) - { - PrintToChat(client, szCMessage); + + int index = C_Format(szCMessage, sizeof(szCMessage)); + + if (index == NO_INDEX) { + PrintToChat(client, "%s", szCMessage); + } + else { + C_SayText2(client, index, szCMessage); + } +} + +/** + * Reples to a message in a command. A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. If the command was from chat, C_PrintToChat() is used. + * Supports color tags. + * + * @param client Client index, or 0 for server. + * @param szMessage Formatting rules. + * @param ... Variable number of format parameters. + * @return No return + * + * On error/Errors: If the client is not connected or invalid. + */ +stock void C_ReplyToCommand(int client, const char[] szMessage, any ...) { + char szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(szCMessage, sizeof(szCMessage), szMessage, 3); + + if (client == 0) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToServer("%s", szCMessage); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToConsole(client, "%s", szCMessage); + } + else { + C_PrintToChat(client, "%s", szCMessage); + } +} + +/** + * Reples to a message in a command. A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. If the command was from chat, C_PrintToChat() is used. + * Supports color tags. + * + * @param client Client index, or 0 for server. + * @param author Author index whose color will be used for teamcolor tag. + * @param szMessage Formatting rules. + * @param ... Variable number of format parameters. + * @return No return + * + * On error/Errors: If the client is not connected or invalid. + */ +stock void C_ReplyToCommandEx(int client, int author, const char[] szMessage, any ...) { + char szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(szCMessage, sizeof(szCMessage), szMessage, 4); + + if (client == 0) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToServer("%s", szCMessage); + } + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + C_RemoveTags(szCMessage, sizeof(szCMessage)); + PrintToConsole(client, "%s", szCMessage); } - else - { - CSayText2(client, index, szCMessage); + else { + C_PrintToChatEx(client, author, "%s", szCMessage); } } @@ -83,20 +161,18 @@ stock CPrintToChat(client, const String:szMessage[], any:...) * @param szMessage Message (formatting rules) * @return No return */ -stock CPrintToChatAll(const String:szMessage[], any:...) -{ - decl String:szBuffer[MAX_MESSAGE_LENGTH]; - - for (new i = 1; i <= MaxClients; i++) - { - if (IsClientInGame(i) && !IsFakeClient(i) && !CSkipList[i]) - { +stock void C_PrintToChatAll(const char[] szMessage, any ...) { + char szBuffer[MAX_MESSAGE_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && !IsFakeClient(i) && !C_SkipList[i]) { SetGlobalTransTarget(i); VFormat(szBuffer, sizeof(szBuffer), szMessage, 2); - CPrintToChat(i, szBuffer); + + C_PrintToChat(i, "%s", szBuffer); } - - CSkipList[i] = false; + + C_SkipList[i] = false; } } @@ -111,31 +187,34 @@ stock CPrintToChatAll(const String:szMessage[], any:...) * * On error/Errors: If the client or author are not connected an error will be thrown. */ -stock CPrintToChatEx(client, author, const String:szMessage[], any:...) -{ - if (client <= 0 || client > MaxClients) +stock void C_PrintToChatEx(int client, int author, const char[] szMessage, any ...) { + if (client <= 0 || client > MaxClients) { ThrowError("Invalid client index %d", client); - - if (!IsClientInGame(client)) + } + + if (!IsClientInGame(client)) { ThrowError("Client %d is not in game", client); - - if (author < 0 || author > MaxClients) + } + + if (author < 0 || author > MaxClients) { ThrowError("Invalid client index %d", author); - - decl String:szBuffer[MAX_MESSAGE_LENGTH]; - decl String:szCMessage[MAX_MESSAGE_LENGTH]; + } + + char szBuffer[MAX_MESSAGE_LENGTH]; + char szCMessage[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage); VFormat(szCMessage, sizeof(szCMessage), szBuffer, 4); - - new index = CFormat(szCMessage, sizeof(szCMessage), author); - if (index == NO_INDEX) - { - PrintToChat(client, szCMessage); + + int index = C_Format(szCMessage, sizeof(szCMessage), author); + + if (index == NO_INDEX) { + PrintToChat(client, "%s", szCMessage); } - else - { - CSayText2(client, author, szCMessage); + else { + C_SayText2(client, author, szCMessage); } } @@ -149,26 +228,26 @@ stock CPrintToChatEx(client, author, const String:szMessage[], any:...) * * On error/Errors: If the author is not connected an error will be thrown. */ -stock CPrintToChatAllEx(author, const String:szMessage[], any:...) -{ - if (author < 0 || author > MaxClients) +stock void C_PrintToChatAllEx(int author, const char[] szMessage, any ...) { + if (author < 0 || author > MaxClients) { ThrowError("Invalid client index %d", author); - - if (!IsClientInGame(author)) + } + + if (!IsClientInGame(author)) { ThrowError("Client %d is not in game", author); - - decl String:szBuffer[MAX_MESSAGE_LENGTH]; - - for (new i = 1; i <= MaxClients; i++) - { - if (IsClientInGame(i) && !IsFakeClient(i) && !CSkipList[i]) - { + } + + char szBuffer[MAX_MESSAGE_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && !IsFakeClient(i) && !C_SkipList[i]) { SetGlobalTransTarget(i); VFormat(szBuffer, sizeof(szBuffer), szMessage, 3); - CPrintToChatEx(i, author, szBuffer); + + C_PrintToChatEx(i, author, "%s", szBuffer); } - - CSkipList[i] = false; + + C_SkipList[i] = false; } } @@ -178,19 +257,55 @@ stock CPrintToChatAllEx(author, const String:szMessage[], any:...) * @param szMessage String. * @return No return */ -stock CRemoveTags(String:szMessage[], maxlength) -{ - for (new i = 0; i < MAX_COLORS; i++) - { - ReplaceString(szMessage, maxlength, CTag[i], ""); +stock void C_RemoveTags(char[] szMessage, int maxlength) { + for (int i = 0; i < MAX_COLORS; i++) { + ReplaceString(szMessage, maxlength, C_Tag[i], "", false); } - - ReplaceString(szMessage, maxlength, "{teamcolor}", ""); + + ReplaceString(szMessage, maxlength, "{teamcolor}", "", false); +} + +/** + * Checks whether a color is allowed or not + * + * @param tag Color Tag. + * @return True when color is supported, otherwise false + */ +stock bool C_ColorAllowed(C_Colors color) { + if (!C_EventIsHooked) { + C_SetupProfile(); + + C_EventIsHooked = true; + } + + return C_Profile_Colors[color]; +} + +/** + * Replace the color with another color + * Handle with care! + * + * @param color color to replace. + * @param newColor color to replace with. + * @noreturn + */ +stock void C_ReplaceColor(C_Colors color, C_Colors newColor) { + if (!C_EventIsHooked) { + C_SetupProfile(); + + C_EventIsHooked = true; + } + + C_Profile_Colors[color] = C_Profile_Colors[newColor]; + C_Profile_TeamIndex[color] = C_Profile_TeamIndex[newColor]; + + C_TagReqSayText2[color] = C_TagReqSayText2[newColor]; + Format(C_TagCode[color], sizeof(C_TagCode[]), C_TagCode[newColor]); } /** * This function should only be used right in front of - * CPrintToChatAll or CPrintToChatAllEx and it tells + * C_PrintToChatAll or C_PrintToChatAllEx and it tells * to those funcions to skip specified client when printing * message to all clients. After message is printed client will * no more be skipped. @@ -198,12 +313,12 @@ stock CRemoveTags(String:szMessage[], maxlength) * @param client Client index * @return No return */ -stock CSkipNextClient(client) -{ - if (client <= 0 || client > MaxClients) +stock void C_SkipNextClient(int client) { + if (client <= 0 || client > MaxClients) { ThrowError("Invalid client index %d", client); - - CSkipList[client] = true; + } + + C_SkipList[client] = true; } /** @@ -215,119 +330,114 @@ stock CSkipNextClient(client) * * On error/Errors: If there is more then one team color is used an error will be thrown. */ -stock CFormat(String:szMessage[], maxlength, author=NO_INDEX) -{ +stock int C_Format(char[] szMessage, int maxlength, int author = NO_INDEX) { /* Hook event for auto profile setup on map start */ - if (!CEventIsHooked) - { - CSetupProfile(); - HookEvent("server_spawn", CEvent_MapStart, EventHookMode_PostNoCopy); - CEventIsHooked = true; + if (!C_EventIsHooked) { + C_SetupProfile(); + HookEvent("server_spawn", C_Event_MapStart, EventHookMode_PostNoCopy); + + C_EventIsHooked = true; } - - new iRandomPlayer = NO_INDEX; - + + int iRandomPlayer = NO_INDEX; + + // On CS:GO set invisible precolor + if (GetEngineVersion() == Engine_CSGO) { + Format(szMessage, maxlength, " %s", szMessage); + } + /* If author was specified replace {teamcolor} tag */ - if (author != NO_INDEX) - { - if (CProfile_SayText2) - { - ReplaceString(szMessage, maxlength, "{teamcolor}", "\x03"); + if (author != NO_INDEX) { + if (C_Profile_SayText2) { + ReplaceString(szMessage, maxlength, "{teamcolor}", "\x03", false); + iRandomPlayer = author; } /* If saytext2 is not supported by game replace {teamcolor} with green tag */ - else - { - ReplaceString(szMessage, maxlength, "{teamcolor}", CTagCode[Color_Green]); + else { + ReplaceString(szMessage, maxlength, "{teamcolor}", C_TagCode[Color_Green], false); } } - else - { - ReplaceString(szMessage, maxlength, "{teamcolor}", ""); + else { + ReplaceString(szMessage, maxlength, "{teamcolor}", "", false); } - + /* For other color tags we need a loop */ - for (new i = 0; i < MAX_COLORS; i++) - { + for (int i = 0; i < MAX_COLORS; i++) { /* If tag not found - skip */ - if (StrContains(szMessage, CTag[i]) == -1) - { + if (StrContains(szMessage, C_Tag[i], false) == -1) { continue; } + /* If tag is not supported by game replace it with green tag */ - else if (!CProfile_Colors[i]) - { - ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green]); + else if (!C_Profile_Colors[i]) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[Color_Green], false); } + /* If tag doesn't need saytext2 simply replace */ - else if (!CTagReqSayText2[i]) - { - ReplaceString(szMessage, maxlength, CTag[i], CTagCode[i]); + else if (!C_TagReqSayText2[i]) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[i], false); } + /* Tag needs saytext2 */ - else - { + else { /* If saytext2 is not supported by game replace tag with green tag */ - if (!CProfile_SayText2) - { - ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green]); + if (!C_Profile_SayText2) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[Color_Green], false); } + /* Game supports saytext2 */ - else - { + else { /* If random player for tag wasn't specified replace tag and find player */ - if (iRandomPlayer == NO_INDEX) - { + if (iRandomPlayer == NO_INDEX) { /* Searching for valid client for tag */ - iRandomPlayer = CFindRandomPlayerByTeam(CProfile_TeamIndex[i]); - + iRandomPlayer = C_FindRandomPlayerByTeam(C_Profile_TeamIndex[i]); + /* If player not found replace tag with green color tag */ - if (iRandomPlayer == NO_PLAYER) - { - ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green]); + if (iRandomPlayer == NO_PLAYER) { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[Color_Green], false); } + /* If player was found simply replace */ - else - { - ReplaceString(szMessage, maxlength, CTag[i], CTagCode[i]); + else { + ReplaceString(szMessage, maxlength, C_Tag[i], C_TagCode[i], false); } - + } /* If found another team color tag throw error */ - else - { - //ReplaceString(szMessage, maxlength, CTag[i], ""); + else { + //ReplaceString(szMessage, maxlength, C_Tag[i], ""); ThrowError("Using two team colors in one message is not allowed"); } } - } } - + return iRandomPlayer; } /** - * Founds a random player with specified team + * Finds a random player with specified team * * @param color_team Client team. * @return Client index or NO_PLAYER if no player found */ -stock CFindRandomPlayerByTeam(color_team) -{ - if (color_team == SERVER_INDEX) - { +stock int C_FindRandomPlayerByTeam(int color_team) { + if (color_team == SERVER_INDEX) { return 0; } - else - { - for (new i = 1; i <= MaxClients; i++) - { - if (IsClientInGame(i) && GetClientTeam(i) == color_team) - { - return i; - } - } + + int[] players = new int[MaxClients]; + int count; + + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && GetClientTeam(i) == color_team) { + players[count++] = i; + } + } + + if (count) { + return players[GetRandomInt(0, count-1)]; } return NO_PLAYER; @@ -341,12 +451,26 @@ stock CFindRandomPlayerByTeam(color_team) * @param szMessage Message * @return No return. */ -stock CSayText2(client, author, const String:szMessage[]) -{ - new Handle:hBuffer = StartMessageOne("SayText2", client); - BfWriteByte(hBuffer, author); - BfWriteByte(hBuffer, true); - BfWriteString(hBuffer, szMessage); +stock void C_SayText2(int client, int author, const char[] szMessage) { + Handle hBuffer = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + + if (GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) { + Protobuf pb = UserMessageToProtobuf(hBuffer); + pb.SetInt("ent_idx", author); + pb.SetBool("chat", true); + pb.SetString("msg_name", szMessage); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + pb.AddString("params", ""); + } + else { + BfWrite bf = UserMessageToBfWrite(hBuffer); + bf.WriteByte(author); + bf.WriteByte(true); + bf.WriteString(szMessage); + } + EndMessage(); } @@ -356,91 +480,426 @@ stock CSayText2(client, author, const String:szMessage[]) * * @return No return. */ -stock CSetupProfile() -{ - decl String:szGameName[30]; - GetGameFolderName(szGameName, sizeof(szGameName)); - - if (StrEqual(szGameName, "cstrike", false)) - { - CProfile_Colors[Color_Lightgreen] = true; - CProfile_Colors[Color_Red] = true; - CProfile_Colors[Color_Blue] = true; - CProfile_Colors[Color_Olive] = true; - CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; - CProfile_TeamIndex[Color_Red] = 2; - CProfile_TeamIndex[Color_Blue] = 3; - CProfile_SayText2 = true; - } - else if (StrEqual(szGameName, "tf", false)) - { - CProfile_Colors[Color_Lightgreen] = true; - CProfile_Colors[Color_Red] = true; - CProfile_Colors[Color_Blue] = true; - CProfile_Colors[Color_Olive] = true; - CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; - CProfile_TeamIndex[Color_Red] = 2; - CProfile_TeamIndex[Color_Blue] = 3; - CProfile_SayText2 = true; - } - else if (StrEqual(szGameName, "left4dead", false) || StrEqual(szGameName, "left4dead2", false)) - { - CProfile_Colors[Color_Lightgreen] = true; - CProfile_Colors[Color_Red] = true; - CProfile_Colors[Color_Blue] = true; - CProfile_Colors[Color_Olive] = true; - CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; - CProfile_TeamIndex[Color_Red] = 3; - CProfile_TeamIndex[Color_Blue] = 2; - CProfile_SayText2 = true; - } - else if (StrEqual(szGameName, "hl2mp", false)) - { +stock void C_SetupProfile() { + EngineVersion engine = GetEngineVersion(); + + if (engine == Engine_CSS) { + C_Profile_Colors[Color_Lightgreen] = true; + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + else if (engine == Engine_CSGO) { + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_Colors[Color_Darkred] = true; + C_Profile_Colors[Color_Lime] = true; + C_Profile_Colors[Color_Lightred] = true; + C_Profile_Colors[Color_Purple] = true; + C_Profile_Colors[Color_Grey] = true; + C_Profile_Colors[Color_Yellow] = true; + C_Profile_Colors[Color_Orange] = true; + C_Profile_Colors[Color_Bluegrey] = true; + C_Profile_Colors[Color_Lightblue] = true; + C_Profile_Colors[Color_Darkblue] = true; + C_Profile_Colors[Color_Grey2] = true; + C_Profile_Colors[Color_Orchid] = true; + C_Profile_Colors[Color_Lightred2] = true; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + else if (engine == Engine_TF2) { + C_Profile_Colors[Color_Lightgreen] = true; + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; + } + else if (engine == Engine_Left4Dead || engine == Engine_Left4Dead2) { + C_Profile_Colors[Color_Lightgreen] = true; + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Lightgreen] = SERVER_INDEX; + C_Profile_TeamIndex[Color_Red] = 3; + C_Profile_TeamIndex[Color_Blue] = 2; + C_Profile_SayText2 = true; + } + else if (engine == Engine_HL2DM) { /* hl2mp profile is based on mp_teamplay convar */ - if (GetConVarBool(FindConVar("mp_teamplay"))) - { - CProfile_Colors[Color_Red] = true; - CProfile_Colors[Color_Blue] = true; - CProfile_Colors[Color_Olive] = true; - CProfile_TeamIndex[Color_Red] = 3; - CProfile_TeamIndex[Color_Blue] = 2; - CProfile_SayText2 = true; + if (GetConVarBool(FindConVar("mp_teamplay"))) { + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_Colors[Color_Olive] = true; + C_Profile_TeamIndex[Color_Red] = 3; + C_Profile_TeamIndex[Color_Blue] = 2; + C_Profile_SayText2 = true; } - else - { - CProfile_SayText2 = false; - CProfile_Colors[Color_Olive] = true; + else { + C_Profile_SayText2 = false; + C_Profile_Colors[Color_Olive] = true; } } - else if (StrEqual(szGameName, "dod", false)) - { - CProfile_Colors[Color_Olive] = true; - CProfile_SayText2 = false; + else if (engine == Engine_DODS) { + C_Profile_Colors[Color_Olive] = true; + C_Profile_SayText2 = false; } /* Profile for other games */ - else - { - if (GetUserMessageId("SayText2") == INVALID_MESSAGE_ID) - { - CProfile_SayText2 = false; + else { + if (GetUserMessageId("SayText2") == INVALID_MESSAGE_ID) { + C_Profile_SayText2 = false; } - else - { - CProfile_Colors[Color_Red] = true; - CProfile_Colors[Color_Blue] = true; - CProfile_TeamIndex[Color_Red] = 2; - CProfile_TeamIndex[Color_Blue] = 3; - CProfile_SayText2 = true; + else { + C_Profile_Colors[Color_Red] = true; + C_Profile_Colors[Color_Blue] = true; + C_Profile_TeamIndex[Color_Red] = 2; + C_Profile_TeamIndex[Color_Blue] = 3; + C_Profile_SayText2 = true; } } } -public Action:CEvent_MapStart(Handle:event, const String:name[], bool:dontBroadcast) -{ - CSetupProfile(); - - for (new i = 1; i <= MaxClients; i++) - { - CSkipList[i] = false; +public void C_Event_MapStart(Event event, const char[] name, bool dontBroadcast) { + C_SetupProfile(); + + for (int i = 1; i <= MaxClients; ++i) { + C_SkipList[i] = false; + } +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * C_ShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock int C_ShowActivity(int client, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char tag[] = "[SM] "; + + char szBuffer[MAX_MESSAGE_LENGTH]; + //char szCMessage[MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s", tag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", tag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 3); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Same as C_ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock int C_ShowActivityEx(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + + char szBuffer[MAX_MESSAGE_LENGTH]; + //char szCMessage[MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + bool display_in_chat = false; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + /* Display the message to the client? */ + if (replyto == SM_REPLY_TO_CONSOLE) { + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToConsole(client, "%s%s", szTag, szBuffer); + display_in_chat = true; + } + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", szTag, szBuffer); + } + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root) || (i == client)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error + */ +stock int C_ShowActivity2(int client, const char[] tag, const char[] format, any ...) { + if (sm_show_activity == null) { + sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + + char szBuffer[MAX_MESSAGE_LENGTH]; + //char szCMessage[MAX_MESSAGE_LENGTH]; + int value = sm_show_activity.IntValue; + // ReplySource replyto = GetCmdReplySource(); + + char name[MAX_NAME_LENGTH] = "Console"; + char sign[MAX_NAME_LENGTH] = "ADMIN"; + if (client != 0) { + if (client < 0 || client > MaxClients || !IsClientConnected(client)) { + ThrowError("Client index %d is invalid", client); + } + + GetClientName(client, name, sizeof(name)); + AdminId id = GetUserAdmin(client); + if (id == INVALID_ADMIN_ID || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + sign = "PLAYER"; + } + + SetGlobalTransTarget(client); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + /* We don't display directly to the console because the chat text + * simply gets added to the console, so we don't want it to print + * twice. + */ + C_PrintToChatEx(client, client, "%s%s", szTag, szBuffer); + } + else { + SetGlobalTransTarget(LANG_SERVER); + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_RemoveTags(szBuffer, sizeof(szBuffer)); + PrintToServer("%s%s", szTag, szBuffer); } -} \ No newline at end of file + + if (!value) { + return 1; + } + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || IsFakeClient(i) || i == client) { + continue; + } + + AdminId id = GetUserAdmin(i); + SetGlobalTransTarget(i); + if (id == INVALID_ADMIN_ID + || !GetAdminFlag(id, Admin_Generic, Access_Effective)) { + /* Treat this as a normal user. */ + if ((value & 1) | (value & 2)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 2)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + else { + /* Treat this as an admin user */ + bool is_root = GetAdminFlag(id, Admin_Root, Access_Effective); + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { + char newsign[MAX_NAME_LENGTH]; + + if ((value & 8) || ((value & 16) && is_root)) { + newsign = name; + } + else { + newsign = sign; + } + + VFormat(szBuffer, sizeof(szBuffer), format, 4); + + C_PrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer); + } + } + } + + return 1; +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc new file mode 100644 index 00000000..bb25aa48 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc @@ -0,0 +1,581 @@ +#if defined _dodgeball_config_included + #endinput +#endif +#define _dodgeball_config_included + +/** + * Handles parsing of configuration files and server commands. + */ + +/** Clears all rocket class definitions and frees associated command DataPacks. */ +void DestroyRocketClasses() +{ + for (int iIndex = 0; iIndex < RocketClassCount; iIndex++) + { + DataPack hCmdOnSpawn = RocketClassCmdsOnSpawn[iIndex]; + DataPack hCmdOnKill = RocketClassCmdsOnKill[iIndex]; + DataPack hCmdOnExplode = RocketClassCmdsOnExplode[iIndex]; + DataPack hCmdOnDeflect = RocketClassCmdsOnDeflect[iIndex]; + DataPack hCmdOnNoTarget = RocketClassCmdsOnNoTarget[iIndex]; + + if (hCmdOnSpawn != null) delete hCmdOnSpawn; + if (hCmdOnKill != null) delete hCmdOnKill; + if (hCmdOnExplode != null) delete hCmdOnExplode; + if (hCmdOnDeflect != null) delete hCmdOnDeflect; + if (hCmdOnNoTarget != null) delete hCmdOnNoTarget; + + RocketClassCmdsOnSpawn[iIndex] = null; + RocketClassCmdsOnKill[iIndex] = null; + RocketClassCmdsOnExplode[iIndex] = null; + RocketClassCmdsOnDeflect[iIndex] = null; + RocketClassCmdsOnNoTarget[iIndex] = null; + } + RocketClassCount = 0; +} + +/** Clears all spawner class definitions and spawn point data. */ +void DestroySpawners() +{ + for (int iIndex = 0; iIndex < SpawnersCount; iIndex++) + { + delete SpawnersChancesTable[iIndex]; + } + + SpawnersCount = 0; + SpawnPointsRedCount = 0; + SpawnPointsBluCount = 0; + DefaultRedSpawner = -1; + DefaultBluSpawner = -1; + SpawnersTrie.Clear(); +} + +/** + * Scans the map for info_target entities and populates spawn point arrays. + * Looks for entities named "rocket_spawn_red", "tf_dodgeball_red", + * "rocket_spawn_blu", or "tf_dodgeball_blu". + * Calls SetFailState if no spawn points are found. + */ +void PopulateSpawnPoints() +{ + SpawnPointsRedCount = 0; + SpawnPointsBluCount = 0; + int iEntity = -1; + while ((iEntity = FindEntityByClassname(iEntity, "info_target")) != -1) + { + char strName[32]; GetEntPropString(iEntity, Prop_Data, "m_iName", strName, sizeof(strName)); + if ((StrContains(strName, "rocket_spawn_red") != -1) || (StrContains(strName, "tf_dodgeball_red") != -1)) + { + int iIndex = FindSpawnerByName(strName); + if (iIndex == -1) iIndex = DefaultRedSpawner; + SpawnPointsRedClass [SpawnPointsRedCount] = iIndex; + SpawnPointsRedEntity[SpawnPointsRedCount] = iEntity; + SpawnPointsRedCount++; + } + + if ((StrContains(strName, "rocket_spawn_blu") != -1) || (StrContains(strName, "tf_dodgeball_blu") != -1)) + { + int iIndex = FindSpawnerByName(strName); + if (iIndex == -1) iIndex = DefaultBluSpawner; + + SpawnPointsBluClass [SpawnPointsBluCount] = iIndex; + SpawnPointsBluEntity[SpawnPointsBluCount] = iEntity; + SpawnPointsBluCount++; + } + } + + // New code block starts here + if (SpawnPointsRedCount == 0 && SpawnPointsBluCount == 0) + { + SetFailState("No 'info_target' entities found on this map."); + return; + } + // New code block ends here + + if (SpawnPointsRedCount == 0) SetFailState("No RED spawn points found on this map."); + if (SpawnPointsBluCount == 0) SetFailState("No BLU spawn points found on this map."); +} + +/** + * Finds a spawner class index by name using the spawners trie. + * + * @param strName The spawner name to search for. + * @return The spawner class index, or -1 if not found. + */ +int FindSpawnerByName(char strName[32]) +{ + int iIndex = -1; + SpawnersTrie.GetValue(strName, iIndex); + return iIndex; +} + +/** Registers server commands for explosion and shockwave effects. */ +void RegisterCommands() +{ + RegServerCmd("tf_dodgeball_explosion", CmdExplosion); + RegServerCmd("tf_dodgeball_shockwave", CmdShockwave); +} + +/** + * Server command: Creates nuke particle effects at a client's position. + * Usage: tf_dodgeball_explosion + * + * @param iArgs Number of command arguments. + * @return Plugin_Handled after processing. + */ +public Action CmdExplosion(int iArgs) +{ + if (!Enabled) + { + CPrintToServer("%t", "Command_Disabled"); + return Plugin_Handled; + } + + if (iArgs != 1) + { + CPrintToServer("%t", "Command_DBShockwave_Usage"); + return Plugin_Handled; + } + + char strBuffer[8]; + int iClient; + GetCmdArg(1, strBuffer, sizeof(strBuffer)); + iClient = StringToInt(strBuffer); + + if (!IsValidClient(iClient)) return Plugin_Handled; + + float fPosition[3]; + GetClientAbsOrigin(iClient, fPosition); + switch (GetURandomIntRange(0, 4)) + { + case 0: PlayParticle(fPosition, PARTICLE_NUKE_1_ANGLES, PARTICLE_NUKE_1); + case 1: PlayParticle(fPosition, PARTICLE_NUKE_2_ANGLES, PARTICLE_NUKE_2); + case 2: PlayParticle(fPosition, PARTICLE_NUKE_3_ANGLES, PARTICLE_NUKE_3); + case 3: PlayParticle(fPosition, PARTICLE_NUKE_4_ANGLES, PARTICLE_NUKE_4); + case 4: PlayParticle(fPosition, PARTICLE_NUKE_5_ANGLES, PARTICLE_NUKE_5); + } + PlayParticle(fPosition, PARTICLE_NUKE_COLLUMN_ANGLES, PARTICLE_NUKE_COLLUMN); + + return Plugin_Handled; +} + +/** + * Server command: Creates a shockwave that damages and pushes players. + * Usage: tf_dodgeball_shockwave + * + * @param iArgs Number of command arguments. + * @return Plugin_Handled after processing. + */ +public Action CmdShockwave(int iArgs) +{ + if (!Enabled) + { + CPrintToServer("%t", "Command_Disabled"); + return Plugin_Handled; + } + + if (iArgs != 5) + { + CPrintToServer("%t", "Command_DBShockwave_Usage"); + return Plugin_Handled; + } + + char strBuffer[32]; + int iClient, iTeam; + float fPosition[3]; + int iDamage; + float fPushStrength, fRadius, fFalloffRadius; + + GetCmdArg(1, strBuffer, sizeof(strBuffer)); iClient = StringToInt(strBuffer); + GetCmdArg(2, strBuffer, sizeof(strBuffer)); iDamage = StringToInt(strBuffer); + GetCmdArg(3, strBuffer, sizeof(strBuffer)); fPushStrength = StringToFloat(strBuffer); + GetCmdArg(4, strBuffer, sizeof(strBuffer)); fRadius = StringToFloat(strBuffer); + GetCmdArg(5, strBuffer, sizeof(strBuffer)); fFalloffRadius = StringToFloat(strBuffer); + + if (!IsValidClient(iClient)) return Plugin_Handled; + + iTeam = GetClientTeam(iClient); + GetClientAbsOrigin(iClient, fPosition); + + for (iClient = 1; iClient <= MaxClients; iClient++) + { + if (!(IsValidClientEx(iClient, true) && (GetClientTeam(iClient) == iTeam))) continue; + + float fPlayerPosition[3]; + GetClientEyePosition(iClient, fPlayerPosition); + float fDistanceToShockwave = GetVectorDistance(fPosition, fPlayerPosition); + + if (!(fDistanceToShockwave < fRadius)) continue; + + float fImpulse[3]; + float fFinalPush; + int iFinalDamage; + fImpulse[0] = fPlayerPosition[0] - fPosition[0]; + fImpulse[1] = fPlayerPosition[1] - fPosition[1]; + fImpulse[2] = fPlayerPosition[2] - fPosition[2]; + NormalizeVector(fImpulse, fImpulse); + + if (fImpulse[2] < 0.4) { fImpulse[2] = 0.4; NormalizeVector(fImpulse, fImpulse); } + + if (fDistanceToShockwave < fFalloffRadius) + { + fFinalPush = fPushStrength; + iFinalDamage = iDamage; + } + else + { + float fImpact = (1.0 - ((fDistanceToShockwave - fFalloffRadius) / (fRadius - fFalloffRadius))); + fFinalPush = fImpact * fPushStrength; + iFinalDamage = RoundToFloor(fImpact * iDamage); + } + + ScaleVector(fImpulse, fFinalPush); + SlapPlayer(iClient, iFinalDamage, true); + SetEntPropVector(iClient, Prop_Data, "m_vecAbsVelocity", fImpulse); + } + + return Plugin_Handled; +} + +/** + * Executes stored commands from a DataPack, replacing placeholders with actual values. + * Used for on-spawn, on-deflect, on-kill, on-explode, and on-no-target commands. + * + * Supported placeholders: + * @name - Rocket class long name + * @rocket - Rocket entity index + * @owner - Owner client index + * @target - Target client index + * @dead - Last dead client index + * @deflections - Number of deflections + * @speed - Current speed (Hammer units/s) + * @mphspeed - Current speed (MPH, rounded) + * @capmphspeed - Capped MPH speed + * @nocapspeed - Uncapped speed + * @2dspeed - Speed with 2 decimal places + * @2dnocapspeed - Uncapped speed with 2 decimal places + * + * @param hDataPack DataPack containing commands to execute. + * @param iClass Rocket class index. + * @param iRocket Rocket entity index. + * @param iOwner Owner client index. + * @param iTarget Target client index. + * @param iLastDead Last dead client index. + * @param fSpeed Current rocket speed (Hammer units/s). + * @param iNumDeflections Number of deflections. + * @param fMphSpeed Current rocket speed (MPH). + */ +void ExecuteCommands(DataPack hDataPack, int iClass, int iRocket, int iOwner, int iTarget, int iLastDead, float fSpeed, int iNumDeflections, float fMphSpeed) +{ + hDataPack.Reset(false); + int iNumCommands = hDataPack.ReadCell(); + + while (iNumCommands-- > 0) + { + char strCmd[256], strBuffer[32]; + + hDataPack.ReadString(strCmd, sizeof(strCmd)); + ReplaceString(strCmd, sizeof(strCmd), "@name", RocketClassLongName[iClass]); + FormatEx(strBuffer, sizeof(strBuffer), "%i", iRocket); ReplaceString(strCmd, sizeof(strCmd), "@rocket", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%i", iOwner); ReplaceString(strCmd, sizeof(strCmd), "@owner", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%i", iTarget); ReplaceString(strCmd, sizeof(strCmd), "@target", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%i", iLastDead); ReplaceString(strCmd, sizeof(strCmd), "@dead", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%i", iNumDeflections); ReplaceString(strCmd, sizeof(strCmd), "@deflections", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%f", fSpeed); ReplaceString(strCmd, sizeof(strCmd), "@speed", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%i", RoundToNearest(fMphSpeed)); ReplaceString(strCmd, sizeof(strCmd), "@mphspeed", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%i", RoundToNearest(fSpeed * HAMMER_TO_MPH)); ReplaceString(strCmd, sizeof(strCmd), "@capmphspeed", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%f", fMphSpeed / HAMMER_TO_MPH); ReplaceString(strCmd, sizeof(strCmd), "@nocapspeed", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%.2f", fSpeed); ReplaceString(strCmd, sizeof(strCmd), "@2dspeed", strBuffer); + FormatEx(strBuffer, sizeof(strBuffer), "%.2f", fMphSpeed / HAMMER_TO_MPH); ReplaceString(strCmd, sizeof(strCmd), "@2dnocapspeed", strBuffer); + + ServerCommand(strCmd); + } +} + +/** + * Parses a dodgeball configuration file (KeyValues format). + * Loads general settings, rocket classes, and spawner classes. + */ +void ParseConfigurations(char[] strConfigFile = "general.cfg") +{ + char strPath[PLATFORM_MAX_PATH]; + char strFileName[PLATFORM_MAX_PATH]; + FormatEx(strFileName, sizeof(strFileName), "configs/dodgeball/%s", strConfigFile); + BuildPath(Path_SM, strPath, sizeof(strPath), strFileName); + LogMessage("Executing configuration file %s", strPath); + + if (!FileExists(strPath, true)) return; + + KeyValues kvConfig = new KeyValues("TF2_Dodgeball"); + if (!kvConfig.ImportFromFile(strPath)) SetFailState("Error while parsing the configuration file."); + + kvConfig.GotoFirstSubKey(); + do + { + char strSection[64]; kvConfig.GetSectionName(strSection, sizeof(strSection)); + if (StrEqual(strSection, "general")) ParseGeneral(kvConfig); + else if (StrEqual(strSection, "classes")) ParseClasses(kvConfig); + else if (StrEqual(strSection, "spawners")) ParseSpawners(kvConfig); + else if (StrEqual(strSection, "presets")) ParsePresets(kvConfig); + } + while (kvConfig.GotoNextKey()); + + delete kvConfig; + Forward_OnRocketsConfigExecuted(strConfigFile); +} + +/** Parses the "general" section of the config (music and scaling settings). */ +void ParseGeneral(KeyValues kvConfig) +{ + // Scaling mode flags (read before music to avoid early return) + UseOrbitCoefficient = view_as(kvConfig.GetNum("orbit coefficient", 0)); + UseTargetSpeedScaling = view_as(kvConfig.GetNum("target speed scaling", 0)); + UseSmoothElevation = view_as(kvConfig.GetNum("smooth elevation", 0)); + UseBounceVerticalScale = view_as(kvConfig.GetNum("bounce vertical scale", 0)); + + MusicEnabled = view_as(kvConfig.GetNum("music", 0)); + if (!MusicEnabled) return; + + UseWebPlayer = view_as(kvConfig.GetNum("use web player", 0)); + kvConfig.GetString("web player url", WebPlayerUrl, sizeof(WebPlayerUrl)); + Music[Music_RoundStart] = kvConfig.GetString("round start", MusicPath[Music_RoundStart], PLATFORM_MAX_PATH) && MusicPath[Music_RoundStart][0]; + Music[Music_RoundWin] = kvConfig.GetString("round end (win)", MusicPath[Music_RoundWin], PLATFORM_MAX_PATH) && MusicPath[Music_RoundWin][0]; + Music[Music_RoundLose] = kvConfig.GetString("round end (lose)", MusicPath[Music_RoundLose], PLATFORM_MAX_PATH) && MusicPath[Music_RoundLose][0]; + Music[Music_Gameplay] = kvConfig.GetString("gameplay", MusicPath[Music_Gameplay], PLATFORM_MAX_PATH) && MusicPath[Music_Gameplay][0]; +} + +/** Parses the "classes" section of the config (rocket class definitions). */ +void ParseClasses(KeyValues kvConfig) +{ + char strName[64]; + char strBuffer[256]; + kvConfig.GotoFirstSubKey(); + do + { + int iIndex = RocketClassCount; + RocketFlags iFlags; + + kvConfig.GetSectionName(strName, sizeof(strName)); strcopy(RocketClassName[iIndex], 16, strName); + kvConfig.GetString("name", strBuffer, sizeof(strBuffer)); strcopy(RocketClassLongName[iIndex], 32, strBuffer); + + if (kvConfig.GetString("model", strBuffer, sizeof(strBuffer))) + { + strcopy(RocketClassModel[iIndex], PLATFORM_MAX_PATH, strBuffer); + if (RocketClassModel[iIndex][0]) + { + iFlags |= RocketFlag_CustomModel; + if (kvConfig.GetNum("is animated", 0)) iFlags |= RocketFlag_IsAnimated; + } + } + + kvConfig.GetString("behaviour", strBuffer, sizeof(strBuffer), "homing"); + if (StrEqual(strBuffer, "homing")) RocketClassBehaviour[iIndex] = Behaviour_Homing; + else if (StrEqual(strBuffer, "legacy homing")) RocketClassBehaviour[iIndex] = Behaviour_LegacyHoming; + else RocketClassBehaviour[iIndex] = Behaviour_Unknown; + + if (kvConfig.GetNum("play spawn sound", 0) == 1) + { + iFlags |= RocketFlag_PlaySpawnSound; + if (kvConfig.GetString("spawn sound", RocketClassSpawnSound[iIndex], PLATFORM_MAX_PATH) && RocketClassSpawnSound[iIndex][0]) + { + iFlags |= RocketFlag_CustomSpawnSound; + } + } + + if (kvConfig.GetNum("play beep sound", 0) == 1) + { + iFlags |= RocketFlag_PlayBeepSound; + RocketClassBeepInterval[iIndex] = kvConfig.GetFloat("beep interval", 0.5); + if (kvConfig.GetString("beep sound", RocketClassBeepSound[iIndex], PLATFORM_MAX_PATH) && RocketClassBeepSound[iIndex][0]) + { + iFlags |= RocketFlag_CustomBeepSound; + } + } + + if (kvConfig.GetNum("play alert sound", 0) == 1) + { + iFlags |= RocketFlag_PlayAlertSound; + if (kvConfig.GetString("alert sound", RocketClassAlertSound[iIndex], PLATFORM_MAX_PATH) && RocketClassAlertSound[iIndex][0]) + { + iFlags |= RocketFlag_CustomAlertSound; + } + } + + if (kvConfig.GetNum("elevate on deflect", 0) == 1) iFlags |= RocketFlag_ElevateOnDeflect; + if (kvConfig.GetNum("neutral rocket", 0) == 1) iFlags |= RocketFlag_IsNeutral; + if (kvConfig.GetNum("keep direction", 0) == 1) iFlags |= RocketFlag_KeepDirection; + if (kvConfig.GetNum("teamless deflects", 0) == 1) iFlags |= RocketFlag_TeamlessHits; + if (kvConfig.GetNum("reset bounces", 0) == 1) iFlags |= RocketFlag_ResetBounces; + if (kvConfig.GetNum("no bounce drags", 0) == 1) iFlags |= RocketFlag_NoBounceDrags; + if (kvConfig.GetNum("can be stolen", 0) == 1) iFlags |= RocketFlag_CanBeStolen; + if (kvConfig.GetNum("steal team check", 0) == 1) iFlags |= RocketFlag_StealTeamCheck; + + RocketClassDamage[iIndex] = kvConfig.GetFloat("damage"); + RocketClassDamageIncrement[iIndex] = kvConfig.GetFloat("damage increment"); + RocketClassCritChance[iIndex] = kvConfig.GetFloat("critical chance"); + RocketClassSpeed[iIndex] = kvConfig.GetFloat("speed"); + RocketClassSpeedIncrement[iIndex] = kvConfig.GetFloat("speed increment"); + if ((RocketClassSpeedLimit[iIndex] = kvConfig.GetFloat("speed limit")) != 0.0) + { + iFlags |= RocketFlag_IsSpeedLimited; + } + + RocketClassTurnRate[iIndex] = kvConfig.GetFloat("turn rate"); + RocketClassTurnRateIncrement[iIndex] = kvConfig.GetFloat("turn rate increment"); + if ((RocketClassTurnRateLimit[iIndex] = kvConfig.GetFloat("turn rate limit")) != 0.0) + { + iFlags |= RocketFlag_IsTRLimited; + } + + RocketClassElevationRate[iIndex] = kvConfig.GetFloat("elevation rate"); + RocketClassElevationLimit[iIndex] = kvConfig.GetFloat("elevation limit"); + RocketClassControlDelay[iIndex] = kvConfig.GetFloat("control delay"); + RocketClassMaxBounces[iIndex] = kvConfig.GetNum("max bounces"); + RocketClassBounceScale[iIndex] = kvConfig.GetFloat("bounce scale", 1.0); + RocketClassPlayerModifier[iIndex] = kvConfig.GetFloat("no. players modifier"); + RocketClassRocketsModifier[iIndex] = kvConfig.GetFloat("no. rockets modifier"); + RocketClassTargetWeight[iIndex] = kvConfig.GetFloat("direction to target weight"); + + // Scaling mode: orbit coefficient + RocketClassOrbitTightness[iIndex] = kvConfig.GetFloat("orbit tightness", 0.0); + + // Scaling mode: target speed + RocketClassMaxSpeed[iIndex] = kvConfig.GetFloat("max speed", 0.0); + RocketClassMaxDeflections[iIndex] = kvConfig.GetNum("max deflections", 0); + if (UseTargetSpeedScaling && RocketClassMaxDeflections[iIndex] > 0) + { + RocketClassSpeedIncrement[iIndex] = (RocketClassMaxSpeed[iIndex] - RocketClassSpeed[iIndex]) / float(RocketClassMaxDeflections[iIndex]); + } + + // Scaling mode: bounce vertical scale + RocketClassBounceVerticalScale[iIndex] = kvConfig.GetFloat("bounce vertical ratio", 1.0); + RocketClassBounceMaxVerticalSpeed[iIndex] = kvConfig.GetFloat("bounce max vertical speed", 0.0); + RocketClassDragPauseDuration[iIndex] = kvConfig.GetFloat("drag pause duration", 0.1); + + DataPack hCmds = null; + kvConfig.GetString("on spawn", strBuffer, sizeof(strBuffer)); + if ((hCmds = ParseCommands(strBuffer)) != null) { iFlags |= RocketFlag_OnSpawnCmd; RocketClassCmdsOnSpawn[iIndex] = hCmds; } + + kvConfig.GetString("on deflect", strBuffer, sizeof(strBuffer)); + if ((hCmds = ParseCommands(strBuffer)) != null) { iFlags |= RocketFlag_OnDeflectCmd; RocketClassCmdsOnDeflect[iIndex] = hCmds; } + + kvConfig.GetString("on kill", strBuffer, sizeof(strBuffer)); + if ((hCmds = ParseCommands(strBuffer)) != null) { iFlags |= RocketFlag_OnKillCmd; RocketClassCmdsOnKill[iIndex] = hCmds; } + + kvConfig.GetString("on explode", strBuffer, sizeof(strBuffer)); + if ((hCmds = ParseCommands(strBuffer)) != null) { iFlags |= RocketFlag_OnExplodeCmd; RocketClassCmdsOnExplode[iIndex] = hCmds; } + + kvConfig.GetString("on no target", strBuffer, sizeof(strBuffer)); + if ((hCmds = ParseCommands(strBuffer)) != null) { iFlags |= RocketFlag_OnNoTargetCmd; RocketClassCmdsOnNoTarget[iIndex] = hCmds; } + + RocketClassFlags[iIndex] = iFlags; + RocketClassCount++; + + if (RocketClassCount >= MAX_ROCKET_CLASSES) + { + LogError("Reached maximum rocket classes (%d). Remaining classes will be ignored.", MAX_ROCKET_CLASSES); + break; + } + } + while (kvConfig.GotoNextKey()); + kvConfig.GoBack(); +} + +/** Parses the "spawners" section of the config (spawner class definitions). */ +void ParseSpawners(KeyValues kvConfig) +{ + char strBuffer[256]; + kvConfig.GotoFirstSubKey(); + do + { + int iIndex = SpawnersCount; + + kvConfig.GetSectionName(strBuffer, sizeof(strBuffer)); strcopy(SpawnersName[iIndex], 32, strBuffer); + SpawnersMaxRockets[iIndex] = kvConfig.GetNum("max rockets", 1); + SpawnersInterval[iIndex] = kvConfig.GetFloat("interval", 1.0); + + SpawnersChancesTable[iIndex] = new ArrayList(); + for (int iClassIndex = 0; iClassIndex < RocketClassCount; iClassIndex++) + { + FormatEx(strBuffer, sizeof(strBuffer), "%s%%", RocketClassName[iClassIndex]); + SpawnersChancesTable[iIndex].Push(kvConfig.GetNum(strBuffer, 0)); + } + + SpawnersTrie.SetValue(SpawnersName[iIndex], iIndex); + SpawnersCount++; + } + while (kvConfig.GotoNextKey()); + + kvConfig.GoBack(); + SpawnersTrie.GetValue("red", DefaultRedSpawner); + SpawnersTrie.GetValue("blu", DefaultBluSpawner); +} + +/** Parses the "presets" section of the config (gameplay preset definitions). */ +void ParsePresets(KeyValues kvConfig) +{ + kvConfig.GotoFirstSubKey(); + do + { + int iIndex = PresetCount; + + kvConfig.GetString("name", PresetName[iIndex], sizeof(PresetName[])); + kvConfig.GetString("rocket class", PresetRocketClass[iIndex], sizeof(PresetRocketClass[])); + PresetMaxRockets[iIndex] = kvConfig.GetNum("max rockets", 1); + PresetSpawnInterval[iIndex] = kvConfig.GetFloat("spawn interval", 0.0); + + PresetCount++; + + if (PresetCount >= MAX_PRESETS) + { + LogError("Reached maximum presets (%d). Remaining presets will be ignored.", MAX_PRESETS); + break; + } + } + while (kvConfig.GotoNextKey()); + kvConfig.GoBack(); +} + +/** + * Parses a semicolon-separated command string into a DataPack. + * Each command between semicolons becomes a separate entry. + * + * @param strLine The command string to parse (e.g., "sm_slay @target; sm_beacon @owner"). + * @return DataPack containing the commands, or null if empty. + */ +DataPack ParseCommands(char[] strLine) +{ + TrimString(strLine); + if (!strLine[0]) return null; + + // Use static array to avoid heap allocation on each call + // This prevents "Not enough space on the heap" when parsing many rocket classes + static char strParts[32][256]; + int iNumParts = ExplodeString(strLine, ";", strParts, 32, 256); + + ArrayList hCommands = new ArrayList(ByteCountToCells(256)); + char strBuffer[256]; + + for (int i = 0; i < iNumParts; i++) + { + TrimString(strParts[i]); + if (strParts[i][0]) hCommands.PushString(strParts[i]); + } + + int iNumCommands = hCommands.Length; + if (iNumCommands == 0) + { + delete hCommands; + return null; + } + + DataPack hDataPack = new DataPack(); + hDataPack.WriteCell(iNumCommands); + for (int i = 0; i < iNumCommands; i++) + { + hCommands.GetString(i, strBuffer, sizeof(strBuffer)); + hDataPack.WriteString(strBuffer); + } + + delete hCommands; + return hDataPack; +} \ No newline at end of file diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc new file mode 100644 index 00000000..db75e923 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc @@ -0,0 +1,200 @@ +#if defined _dodgeball_core_included + #endinput +#endif +#define _dodgeball_core_included + +/** + * Contains core gamemode logic: enabling/disabling the mod and the main game timer. + */ + +// Tracks whether game events have been hooked (to prevent double-unhook errors) +static bool EventsHooked = false; + +/** Checks if the current map is a dodgeball map (starts with "tfdb_", "db_", or "dbs_"). */ +bool IsDodgeBallMap() +{ + char map[64]; GetCurrentMap(map, sizeof(map)); + GetMapDisplayName(map, map, sizeof(map)); + return (StrContains(map, "tfdb_", false) == 0 || + StrContains(map, "db_", false) == 0 || + StrContains(map, "dbs_", false) == 0); +} + +/** + * Enables the dodgeball gamemode. Loads configuration, hooks events, + * precaches sounds/models, and starts the game timer. + */ +void EnableDodgeBall() +{ + if (Enabled) return; + + char mapName[64]; GetCurrentMap(mapName, sizeof(mapName)); + GetMapDisplayName(mapName, mapName, sizeof(mapName)); + char mapFile[PLATFORM_MAX_PATH]; FormatEx(mapFile, sizeof(mapFile), "%s.cfg", mapName); + + ParseConfigurations(); + ParseConfigurations("presets.cfg"); + ParseConfigurations(mapFile); + + if (RocketClassCount == 0) SetFailState("No rocket class defined."); + if (SpawnersCount == 0) SetFailState("No spawner class defined."); + if (DefaultRedSpawner == -1) SetFailState("No spawner class definition for the Red spawners exists in the config file."); + if (DefaultBluSpawner == -1) SetFailState("No spawner class definition for the Blu spawners exists in the config file."); + + // Only hook events if not already hooked + if (!EventsHooked) + { + HookEventEx("teamplay_round_start", OnRoundStart, EventHookMode_PostNoCopy); + HookEventEx("teamplay_round_active", OnSetupFinished, EventHookMode_PostNoCopy); // For non-Arena modes + HookEventEx("arena_round_start", OnSetupFinished, EventHookMode_PostNoCopy); // For Arena mode + HookEventEx("teamplay_round_win", OnRoundEnd, EventHookMode_PostNoCopy); + HookEventEx("player_spawn", OnPlayerSpawn, EventHookMode_Post); + HookEventEx("player_death", OnPlayerDeath, EventHookMode_Pre); + HookEventEx("post_inventory_application", OnPlayerInventory, EventHookMode_Post); + HookEventEx("teamplay_broadcast_audio", OnBroadcastAudio, EventHookMode_Pre); + HookEventEx("object_deflected", OnObjectDeflected); + EventsHooked = true; + } + + AddMultiTargetFilter("@stealer", MLTargetFilterStealer, "last stealer", false); + AddMultiTargetFilter("@!stealer", MLTargetFilterStealer, "non last stealer", false); + + PrecacheSound(SOUND_DEFAULT_SPAWN, true); + PrecacheSound(SOUND_DEFAULT_BEEP, true); + PrecacheSound(SOUND_DEFAULT_ALERT, true); + PrecacheSound(SOUND_DEFAULT_SPEEDUP, true); + + if (MusicEnabled) + { + if (Music[Music_RoundStart]) PrecacheSoundEx(MusicPath[Music_RoundStart], true, true); + if (Music[Music_RoundWin]) PrecacheSoundEx(MusicPath[Music_RoundWin], true, true); + if (Music[Music_RoundLose]) PrecacheSoundEx(MusicPath[Music_RoundLose], true, true); + if (Music[Music_Gameplay]) PrecacheSoundEx(MusicPath[Music_Gameplay], true, true); + } + + PrecacheParticle(PARTICLE_NUKE_1); + PrecacheParticle(PARTICLE_NUKE_2); + PrecacheParticle(PARTICLE_NUKE_3); + PrecacheParticle(PARTICLE_NUKE_4); + PrecacheParticle(PARTICLE_NUKE_5); + PrecacheParticle(PARTICLE_NUKE_COLLUMN); + + for (int i = 0; i < RocketClassCount; i++) + { + RocketFlags flags = RocketClassFlags[i]; + if (TestFlags(flags, RocketFlag_CustomModel)) PrecacheModelEx(RocketClassModel[i], true, true); + if (TestFlags(flags, RocketFlag_CustomSpawnSound)) PrecacheSoundEx(RocketClassSpawnSound[i], true, true); + if (TestFlags(flags, RocketFlag_CustomBeepSound)) PrecacheSoundEx(RocketClassBeepSound[i], true, true); + if (TestFlags(flags, RocketFlag_CustomAlertSound)) PrecacheSoundEx(RocketClassAlertSound[i], true, true); + } + + char cfgFile[64]; + CvarEnableCfgFile.GetString(cfgFile, sizeof(cfgFile)); + ServerCommand("exec \"%s\"", cfgFile); + + Enabled = true; + RoundStarted = false; + RoundCount = 0; +} + +/** + * Disables the dodgeball gamemode. Cleans up rockets, unhooks events, + * and resets state. + */ +void DisableDodgeBall() +{ + if (!Enabled) return; + + DestroyRockets(); + DestroyRocketClasses(); + DestroySpawners(); + PresetCount = 0; + + if (LogicTimer != null) KillTimer(LogicTimer); + LogicTimer = null; + + Music[Music_RoundStart] = + Music[Music_RoundWin] = + Music[Music_RoundLose] = + Music[Music_Gameplay] = false; + + // Only unhook events if they were previously hooked + if (EventsHooked) + { + UnhookEvent("teamplay_round_start", OnRoundStart, EventHookMode_PostNoCopy); + UnhookEvent("teamplay_round_active", OnSetupFinished, EventHookMode_PostNoCopy); + UnhookEvent("arena_round_start", OnSetupFinished, EventHookMode_PostNoCopy); + UnhookEvent("teamplay_round_win", OnRoundEnd, EventHookMode_PostNoCopy); + UnhookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post); + UnhookEvent("player_death", OnPlayerDeath, EventHookMode_Pre); + UnhookEvent("post_inventory_application", OnPlayerInventory, EventHookMode_Post); + UnhookEvent("teamplay_broadcast_audio", OnBroadcastAudio, EventHookMode_Pre); + UnhookEvent("object_deflected", OnObjectDeflected); + EventsHooked = false; + } + + RemoveMultiTargetFilter("@stealer", MLTargetFilterStealer); + RemoveMultiTargetFilter("@!stealer", MLTargetFilterStealer); + + char cfgFile[64]; CvarDisableCfgFile.GetString(cfgFile, sizeof(cfgFile)); + ServerCommand("exec \"%s\"", cfgFile); + + Enabled = false; + RoundStarted = false; + RoundCount = 0; +} + +/** + * This is the main logic timer for the gamemode. + * It handles rocket spawning and the logic for "legacy homing" rockets. + * It also calls shared logic functions like sound updates. + */ +public Action OnDodgeBallGameFrame(Handle timer, any data) +{ + if (!BothTeamsPlaying()) return Plugin_Continue; + + // Rocket Spawning Logic + if (GetGameTime() >= NextSpawnTime) + { + if (LastDeadTeam == view_as(TFTeam_Red)) + { + int spawnerEntity = SpawnPointsRedEntity[CurrentRedSpawn]; + int spawnerClass = SpawnPointsRedClass[CurrentRedSpawn]; + + if (RocketCount < SpawnersMaxRockets[spawnerClass]) + { + CreateRocket(spawnerEntity, spawnerClass, view_as(TFTeam_Red)); + CurrentRedSpawn = (CurrentRedSpawn + 1) % SpawnPointsRedCount; + } + } + else + { + int spawnerEntity = SpawnPointsBluEntity[CurrentBluSpawn]; + int spawnerClass = SpawnPointsBluClass[CurrentBluSpawn]; + + if (RocketCount < SpawnersMaxRockets[spawnerClass]) + { + CreateRocket(spawnerEntity, spawnerClass, view_as(TFTeam_Blue)); + CurrentBluSpawn = (CurrentBluSpawn + 1) % SpawnPointsBluCount; + } + } + } + + // Rocket Think Logic + int index = -1; + while ((index = FindNextValidRocket(index)) != -1) + { + // This timer handles legacy homing rockets and shared logic for all rockets. + switch (RocketClassBehaviour[RocketClass[index]]) + { + case Behaviour_LegacyHoming: + { + RocketLegacyThink(index); + } + } + // Shared logic (sounds, delays) is called for all rockets from here. + SharedRocketThink(index); + } + + return Plugin_Continue; +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc new file mode 100644 index 00000000..aa066c71 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc @@ -0,0 +1,453 @@ +#if defined _dodgeball_events_included + #endinput +#endif +#define _dodgeball_events_included + +/** + * Handles all game events hooked by the plugin. + */ + +/** Called when a new round starts. Resets steal info and plays round start music. */ +public void OnRoundStart(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + if (Music[Music_RoundStart]) + { + EmitSoundToAll(MusicPath[Music_RoundStart]); + } + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + StealInfo[iClient].stoleRocket = false; + StealInfo[iClient].rocketsStolen = 0; + } +} + +/** Called when setup phase ends. Initializes spawn points and starts the game timer. */ +public void OnSetupFinished(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + // Guard against being called twice (both teamplay_round_active and arena_round_start hook this) + if (RoundStarted) return; + if (!BothTeamsPlaying()) return; + + PopulateSpawnPoints(); + if (LastDeadTeam == 0) LastDeadTeam = GetURandomIntRange(view_as(TFTeam_Red), view_as(TFTeam_Blue)); + if (!IsValidClient(LastDeadClient)) LastDeadClient = 0; + + LogicTimer = CreateTimer(FPS_LOGIC_INTERVAL, OnDodgeBallGameFrame, _, TIMER_REPEAT); + PlayerCount = CountAlivePlayers(); + RocketsFired = 0; + CurrentRedSpawn = 0; + CurrentBluSpawn = 0; + NextSpawnTime = GetGameTime(); + RoundStarted = true; + RoundCount++; +} + +/** Called when round ends. Stops timer, music, and destroys all rockets. */ +public void OnRoundEnd(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + if (LogicTimer != null) + { + KillTimer(LogicTimer); + LogicTimer = null; + } + + if (MusicEnabled) + { + if (UseWebPlayer) + { + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClientEx(iClient)) continue; + ShowHiddenMOTDPanel(iClient, "MusicPlayerStop", "http://0.0.0.0/"); + } + } + else if (Music[Music_Gameplay]) + { + StopSoundToAll(SNDCHAN_MUSIC, MusicPath[Music_Gameplay]); + } + } + + DestroyRockets(); + RoundStarted = false; +} + +/** Called when player spawns. Forces Pyro class for dodgeball. */ +public void OnPlayerSpawn(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iClient = GetClientOfUserId(hEvent.GetInt("userid")); + if (!IsValidClient(iClient)) return; + + TFClassType iClass = TF2_GetPlayerClass(iClient); + if ((iClass == TFClass_Pyro || iClass == TFClass_Unknown)) return; + + TF2_SetPlayerClass(iClient, TFClass_Pyro, false, true); + TF2_RespawnPlayer(iClient); +} + +/** Called when player dies. Updates game state and executes kill commands. */ +public void OnPlayerDeath(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + if (!RoundStarted) return; + + SetRandomSeed(view_as(GetGameTime())); + + int iAttacker = GetClientOfUserId(hEvent.GetInt("attacker")); + int iVictim = GetClientOfUserId(hEvent.GetInt("userid")); + + if (!IsValidClient(iVictim)) return; + + StealInfo[iVictim].stoleRocket = false; + StealInfo[iVictim].rocketsStolen = 0; + + LastDeadClient = iVictim; + LastDeadTeam = GetClientTeam(iVictim); + PlayerCount = CountAlivePlayers(); + + int iInflictor = hEvent.GetInt("inflictor_entindex"); + int iIndex = FindRocketByEntity(iInflictor); + + if (iIndex == -1) return; + + int iClass = RocketClass[iIndex]; + int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); + float fSpeed = RocketSpeed[iIndex]; + float fMphSpeed = RocketMphSpeed[iIndex]; + int iDeflections = RocketDeflections[iIndex]; + + if ((RocketInstanceFlags[iIndex] & RocketFlag_OnExplodeCmd) && !(RocketInstanceFlags[iIndex] & RocketFlag_Exploded)) + { + ExecuteCommands(RocketClassCmdsOnExplode[iClass], iClass, iInflictor, iAttacker, iTarget, LastDeadClient, fSpeed, iDeflections, fMphSpeed); + RocketInstanceFlags[iIndex] |= RocketFlag_Exploded; + } + + if (TestFlags(RocketInstanceFlags[iIndex], RocketFlag_OnKillCmd)) + { + ExecuteCommands(RocketClassCmdsOnKill[iClass], iClass, iInflictor, iAttacker, iTarget, LastDeadClient, fSpeed, iDeflections, fMphSpeed); + } +} + +/** Called when player receives inventory. Removes non-flamethrower weapons. */ +public void OnPlayerInventory(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iClient = GetClientOfUserId(hEvent.GetInt("userid")); + + if (!IsValidClient(iClient)) return; + for (int iSlot = 1; iSlot < 5; iSlot++) + { + int iEntity = GetPlayerWeaponSlot(iClient, iSlot); + if (iEntity != -1) RemoveEntity(iEntity); + } +} + +/** Blocks primary attack when dodgeball is enabled. */ +public Action OnPlayerRunCmd(int iClient, int &iButtons, int &iImpulse, float fVelocity[3], float fAngles[3], int &iWeapon) +{ + if (Enabled) iButtons &= ~IN_ATTACK; + return Plugin_Continue; +} + +/** Handles custom music for round start, win, and lose events. */ +public Action OnBroadcastAudio(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + if (!MusicEnabled) return Plugin_Continue; + + char strSound[PLATFORM_MAX_PATH]; + hEvent.GetString("sound", strSound, sizeof(strSound)); + int iTeam = hEvent.GetInt("team"); + + if (StrEqual(strSound, "Announcer.AM_RoundStartRandom")) + { + if (!UseWebPlayer) + { + if (!Music[Music_Gameplay]) return Plugin_Continue; + EmitSoundToAll(MusicPath[Music_Gameplay], SOUND_FROM_PLAYER, SNDCHAN_MUSIC); + return Plugin_Handled; + } + else + { + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClientEx(iClient)) continue; + ShowHiddenMOTDPanel(iClient, "MusicPlayerStart", WebPlayerUrl); + } + return Plugin_Handled; + } + } + else if (StrEqual(strSound, "Game.YourTeamWon")) + { + if (!Music[Music_RoundWin]) return Plugin_Continue; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!(IsValidClientEx(iClient) && (iTeam == GetClientTeam(iClient)))) continue; + EmitSoundToClient(iClient, MusicPath[Music_RoundWin]); + } + return Plugin_Handled; + } + else if (StrEqual(strSound, "Game.YourTeamLost")) + { + if (!Music[Music_RoundLose]) return Plugin_Continue; + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!(IsValidClientEx(iClient) && (iTeam == GetClientTeam(iClient)))) continue; + EmitSoundToClient(iClient, MusicPath[Music_RoundLose]); + } + return Plugin_Handled; + } + + return Plugin_Continue; +} + +/** Called when a projectile is deflected. Updates deflection count and drag state. */ +public void OnObjectDeflected(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iEntity = hEvent.GetInt("object_entindex"); + int iIndex = FindRocketByEntity(iEntity); + if (iIndex == -1) return; + + RocketEventDeflections[iIndex]++; + + int iLauncher = GetEntPropEnt(iEntity, Prop_Send, "m_hLauncher"); + if (iLauncher != -1) + { + SetEntPropEnt(iEntity, Prop_Send, "m_hOriginalLauncher", iLauncher); + } + + if (RocketInstanceState[iIndex] & RocketState_Delayed) + { + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~RocketState_Delayed)); + RocketSpeed[iIndex] = CalculateRocketSpeed(RocketClass[iIndex], CalculateModifier(RocketClass[iIndex], RocketDeflections[iIndex])); + } + + if (RocketInstanceFlags[iIndex] & RocketFlag_ResetBounces) + { + RocketBounces[iIndex] = 0; + } + + if (RocketInstanceFlags[iIndex] & RocketFlag_IsNeutral) + { + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); + } +} + +/** + * This hook runs every server frame. + * It's responsible for the smooth "homing" behaviour. + * "legacy homing" is handled by the logic timer OnDodgeBallGameFrame (~10Hz). + */ +public void OnGameFrame() +{ + if (!Enabled || !RoundStarted) return; + + int iIndex = -1; + while ((iIndex = FindNextValidRocket(iIndex)) != -1) + { + if (RocketClassBehaviour[RocketClass[iIndex]] == Behaviour_Homing) + { + HomingRocketThink(iIndex); + } + } +} + +/** + * Cleans up client state when they disconnect. + * + * @param iClient Client index that disconnected. + */ +public void OnClientDisconnect(int iClient) +{ + StealInfo[iClient].stoleRocket = false; + StealInfo[iClient].rocketsStolen = 0; + + if (iClient == LastDeadClient) + { + LastDeadClient = 0; + } + + if (iClient == LastStealer) + { + LastStealer = 0; + } +} + +/** + * SDKHook callback for rocket collision detection. + * Handles initial checks before bouncing and player collision. + * + * @param iEntity The rocket entity. + * @param iOther The entity being touched. + * @return Plugin_Handled to block touch, Plugin_Continue otherwise. + */ +public Action OnStartTouch(int iEntity, int iOther) +{ + int iIndex = FindRocketByEntity(iEntity); + + if (iIndex == -1) return Plugin_Continue; + if (iOther > 0 && iOther <= MaxClients) + { + if (RocketDeflections[iIndex] == 0) + { + SetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity", 0); + SetEntPropEnt(iEntity, Prop_Send, "m_hOriginalLauncher", -1); + SetEntPropEnt(iEntity, Prop_Send, "m_hLauncher", -1); + } + return Plugin_Continue; + } + + int iClass = RocketClass[iIndex]; + if (RocketBounces[iIndex] >= RocketClassMaxBounces[iClass]) + { + if (RocketDeflections[iIndex] == 0) + { + SetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity", 0); + SetEntPropEnt(iEntity, Prop_Send, "m_hOriginalLauncher", -1); + SetEntPropEnt(iEntity, Prop_Send, "m_hLauncher", -1); + } + return Plugin_Continue; + } + + SDKHook(iEntity, SDKHook_Touch, OnTouch); + return Plugin_Handled; +} + +public Action OnTouch(int iEntity, int iOther) +{ + int iIndex = FindRocketByEntity(iEntity); + + if (iIndex == -1) + { + SDKUnhook(iEntity, SDKHook_Touch, OnTouch); + return Plugin_Continue; + } + + int iClass = RocketClass[iIndex]; + + float vOrigin[3]; + GetEntPropVector(iEntity, Prop_Data, "m_vecOrigin", vOrigin); + + float vAngles[3]; + GetEntPropVector(iEntity, Prop_Data, "m_angRotation", vAngles); + + float vVelocity[3]; + GetEntPropVector(iEntity, Prop_Data, "m_vecAbsVelocity", vVelocity); + + Handle hTrace = TR_TraceRayFilterEx(vOrigin, vAngles, MASK_SHOT, RayType_Infinite, TEF_ExcludeEntity, iEntity); + + if (!TR_DidHit(hTrace)) + { + delete hTrace; + return Plugin_Continue; + } + + float vNormal[3]; + TR_GetPlaneNormal(hTrace, vNormal); + delete hTrace; + + float dotProduct = GetVectorDotProduct(vNormal, vVelocity); + ScaleVector(vNormal, dotProduct); + ScaleVector(vNormal, 2.0); + + float vBounceVec[3]; + SubtractVectors(vVelocity, vNormal, vBounceVec); + ScaleVector(vBounceVec, RocketClassBounceScale[iClass]); + + // Z-clamp: scale vertical component to prevent extreme vertical bounces + if (UseBounceVerticalScale && RocketClassBounceVerticalScale[iClass] < 1.0) + { + vBounceVec[2] *= RocketClassBounceVerticalScale[iClass]; + } + + // Absolute vertical speed cap (like UDL's crawl bounce max up) + if (UseBounceVerticalScale && RocketClassBounceMaxVerticalSpeed[iClass] > 0.0) + { + if (vBounceVec[2] > RocketClassBounceMaxVerticalSpeed[iClass]) + { + vBounceVec[2] = RocketClassBounceMaxVerticalSpeed[iClass]; + } + else if (vBounceVec[2] < -RocketClassBounceMaxVerticalSpeed[iClass]) + { + vBounceVec[2] = -RocketClassBounceMaxVerticalSpeed[iClass]; + } + } + + float vNewAngles[3]; + GetVectorAngles(vBounceVec, vNewAngles); + + float vNewAnglesRef[3]; CopyVectors(vNewAngles, vNewAnglesRef); + float vBounceVecRef[3]; + CopyVectors(vBounceVec, vBounceVecRef); + + Action aResult = Forward_OnRocketBouncePre(iIndex, iEntity, vNewAnglesRef, vBounceVecRef); + if (aResult == Plugin_Stop || aResult == Plugin_Handled) + { + SDKUnhook(iEntity, SDKHook_Touch, OnTouch); + return Plugin_Continue; + } + else if (aResult == Plugin_Changed) + { + CopyVectors(vNewAnglesRef, vNewAngles); + CopyVectors(vBounceVecRef, vBounceVec); + } + + // Pause homing — rocket flies freely with bounce velocity until + // the logic timer re-enables it (bounce pauses use timer, not per-frame drag). + RocketHomingPaused[iIndex] = true; + RocketIsDragPause[iIndex] = false; + + TeleportEntity(iEntity, NULL_VECTOR, vNewAngles, vBounceVec); + RocketBounces[iIndex]++; + if (RocketInstanceFlags[iIndex] & RocketFlag_NoBounceDrags) + { + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~RocketState_CanDrag)); + } + + if ((RocketInstanceState[iIndex] & RocketState_Delayed) || (RocketInstanceFlags[iIndex] & RocketFlag_KeepDirection)) + { + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] | RocketState_Bouncing)); + } + else + { + float fDirection[3]; + GetAngleVectors(vNewAngles,fDirection,NULL_VECTOR,NULL_VECTOR); + CopyVectors(fDirection, RocketDirection[iIndex]); + } + + Forward_OnRocketBounce(iIndex, iEntity); + SDKUnhook(iEntity, SDKHook_Touch, OnTouch); + + return Plugin_Handled; +} + +public Action OnTFExplosion(const char[] strTEName, const int[] iClients, int iNumClients, float fDelay) +{ + static int bIgnoreHook; + if (!Enabled) + { + return Plugin_Continue; + } + + if (bIgnoreHook) + { + bIgnoreHook = false; + return Plugin_Continue; + } + + TE_Start("TFExplosion"); + + float vecNormal[3]; TE_ReadVector("m_vecNormal", vecNormal); + TE_WriteFloat("m_vecOrigin[0]", TE_ReadFloat("m_vecOrigin[0]")); + TE_WriteFloat("m_vecOrigin[1]", TE_ReadFloat("m_vecOrigin[1]")); + TE_WriteFloat("m_vecOrigin[2]", TE_ReadFloat("m_vecOrigin[2]")); + TE_WriteVector("m_vecNormal", vecNormal); + TE_WriteNum("m_iWeaponID", TE_ReadNum("m_iWeaponID")); + TE_WriteNum("entindex", TE_ReadNum("entindex")); + TE_WriteNum("m_nDefID", -1); + TE_WriteNum("m_nSound", TE_ReadNum("m_nSound")); + TE_WriteNum("m_iCustomParticleIndex", TE_ReadNum("m_iCustomParticleIndex")); + + bIgnoreHook = true; + TE_Send(iClients, iNumClients, fDelay); + return Plugin_Stop; + +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc new file mode 100644 index 00000000..80b19f38 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc @@ -0,0 +1,810 @@ +#if defined _dodgeball_natives_included + #endinput +#endif +#define _dodgeball_natives_included + +/** + * Implements the native functions exposed by the TF2 Dodgeball plugin. + * These natives allow other plugins to interact with and control the gamemode. + */ + +// ********************************************************************************* +// NATIVES - GENERAL & STATE +// ********************************************************************************* + +public any Native_IsValidRocket(Handle hPlugin, int iNumParams) { + return IsValidRocket(GetNativeCell(1)); +} + +public any Native_FindRocketByEntity(Handle hPlugin, int iNumParams) { + return FindRocketByEntity(GetNativeCell(1)); +} + +public any Native_IsDodgeballEnabled(Handle hPlugin, int iNumParams) { + return Enabled; +} + +// ********************************************************************************* +// NATIVES - ROCKET GETTERS/SETTERS +// ********************************************************************************* + +public any Native_GetRocketEntity(Handle hPlugin, int iNumParams) { + return EntRefToEntIndex(RocketEntity[GetNativeCell(1)]); +} + +public any Native_SetRocketEntity(Handle hPlugin, int iNumParams) { + RocketEntity[GetNativeCell(1)] = EntIndexToEntRef(GetNativeCell(2)); + return 0; +} + +public any Native_GetRocketFlags(Handle hPlugin, int iNumParams) { + return RocketInstanceFlags[GetNativeCell(1)]; +} + +public any Native_SetRocketFlags(Handle hPlugin, int iNumParams) { + RocketInstanceFlags[GetNativeCell(1)] = view_as(GetNativeCell(2)); + return 0; +} + +public any Native_GetRocketTarget(Handle hPlugin, int iNumParams) { + return EntRefToEntIndex(RocketTarget[GetNativeCell(1)]); +} + +public any Native_SetRocketTarget(Handle hPlugin, int iNumParams) { + RocketTarget[GetNativeCell(1)] = EntIndexToEntRef(GetNativeCell(2)); + return 0; +} + +public any Native_GetRocketEventDeflections(Handle hPlugin, int iNumParams) { + return RocketEventDeflections[GetNativeCell(1)]; +} + +public any Native_SetRocketEventDeflections(Handle hPlugin, int iNumParams) { + RocketEventDeflections[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketDeflections(Handle hPlugin, int iNumParams) { + return RocketDeflections[GetNativeCell(1)]; +} + +public any Native_SetRocketDeflections(Handle hPlugin, int iNumParams) { + RocketDeflections[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClass(Handle hPlugin, int iNumParams) { + return RocketClass[GetNativeCell(1)]; +} + +public any Native_SetRocketClass(Handle hPlugin, int iNumParams) { + RocketClass[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketSpeed(Handle hPlugin, int iNumParams) { + return view_as(RocketSpeed[GetNativeCell(1)]); +} + +public any Native_SetRocketSpeed(Handle hPlugin, int iNumParams) { + RocketSpeed[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketMphSpeed(Handle hPlugin, int iNumParams) { + return view_as(RocketMphSpeed[GetNativeCell(1)]); +} + +public any Native_SetRocketMphSpeed(Handle hPlugin, int iNumParams) { + RocketMphSpeed[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketDirection(Handle hPlugin, int iNumParams) { + SetNativeArray(2, RocketDirection[GetNativeCell(1)], 3); + return 0; +} + +public any Native_SetRocketDirection(Handle hPlugin, int iNumParams) { + float fDirection[3]; + GetNativeArray(2, fDirection, sizeof(fDirection)); + CopyVectors(fDirection, RocketDirection[GetNativeCell(1)]); + return 0; +} + +public any Native_GetRocketLastDeflectionTime(Handle hPlugin, int iNumParams) { + return view_as(RocketLastDeflectionTime[GetNativeCell(1)]); +} + +public any Native_SetRocketLastDeflectionTime(Handle hPlugin, int iNumParams) { + RocketLastDeflectionTime[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketLastBeepTime(Handle hPlugin, int iNumParams) { + return view_as(RocketLastBeepTime[GetNativeCell(1)]); +} + +public any Native_SetRocketLastBeepTime(Handle hPlugin, int iNumParams) { + RocketLastBeepTime[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketCount(Handle hPlugin, int iNumParams) { + return RocketCount; +} + +public any Native_GetLastSpawnTime(Handle hPlugin, int iNumParams) { + return view_as(LastSpawnTime[GetNativeCell(1)]); +} + +public any Native_GetRocketBounces(Handle hPlugin, int iNumParams) { + return RocketBounces[GetNativeCell(1)]; +} + +public any Native_SetRocketBounces(Handle hPlugin, int iNumParams) { + RocketBounces[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketState(Handle hPlugin, int iNumParams) { + return RocketInstanceState[GetNativeCell(1)]; +} + +public any Native_SetRocketState(Handle hPlugin, int iNumParams) { + Internal_SetRocketState(GetNativeCell(1), view_as(GetNativeCell(2))); + return 0; +} + +// ********************************************************************************* +// NATIVES - ROCKET CLASS GETTERS/SETTERS +// ********************************************************************************* + +public any Native_GetRocketClassCount(Handle hPlugin, int iNumParams) { + return RocketClassCount; +} + +public any Native_SetRocketClassCount(Handle hPlugin, int iNumParams) { + RocketClassCount = GetNativeCell(1); + return 0; +} + +public any Native_GetRocketClassBehaviour(Handle hPlugin, int iNumParams) { + return RocketClassBehaviour[GetNativeCell(1)]; +} + +public any Native_SetRocketClassBehaviour(Handle hPlugin, int iNumParams) { + RocketClassBehaviour[GetNativeCell(1)] = view_as(GetNativeCell(2)); + return 0; +} + +public any Native_GetRocketClassFlags(Handle hPlugin, int iNumParams) { + return RocketClassFlags[GetNativeCell(1)]; +} + +public any Native_SetRocketClassFlags(Handle hPlugin, int iNumParams) { + RocketClassFlags[GetNativeCell(1)] = view_as(GetNativeCell(2)); + return 0; +} + +public any Native_GetRocketClassDamage(Handle hPlugin, int iNumParams) { + return view_as(RocketClassDamage[GetNativeCell(1)]); +} + +public any Native_SetRocketClassDamage(Handle hPlugin, int iNumParams) { + RocketClassDamage[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassDamageIncrement(Handle hPlugin, int iNumParams) { + return view_as(RocketClassDamageIncrement[GetNativeCell(1)]); +} + +public any Native_SetRocketClassDamageIncrement(Handle hPlugin, int iNumParams) { + RocketClassDamageIncrement[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassSpeed(Handle hPlugin, int iNumParams) { + return view_as(RocketClassSpeed[GetNativeCell(1)]); +} + +public any Native_SetRocketClassSpeed(Handle hPlugin, int iNumParams) { + RocketClassSpeed[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassSpeedIncrement(Handle hPlugin, int iNumParams) { + return view_as(RocketClassSpeedIncrement[GetNativeCell(1)]); +} + +public any Native_SetRocketClassSpeedIncrement(Handle hPlugin, int iNumParams) { + RocketClassSpeedIncrement[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassSpeedLimit(Handle hPlugin, int iNumParams) { + return view_as(RocketClassSpeedLimit[GetNativeCell(1)]); +} + +public any Native_SetRocketClassSpeedLimit(Handle hPlugin, int iNumParams) { + RocketClassSpeedLimit[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassTurnRate(Handle hPlugin, int iNumParams) { + return view_as(RocketClassTurnRate[GetNativeCell(1)]); +} + +public any Native_SetRocketClassTurnRate(Handle hPlugin, int iNumParams) { + RocketClassTurnRate[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassTurnRateIncrement(Handle hPlugin, int iNumParams) { + return view_as(RocketClassTurnRateIncrement[GetNativeCell(1)]); +} + +public any Native_SetRocketClassTurnRateIncrement(Handle hPlugin, int iNumParams) { + RocketClassTurnRateIncrement[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassTurnRateLimit(Handle hPlugin, int iNumParams) { + return view_as(RocketClassTurnRateLimit[GetNativeCell(1)]); +} + +public any Native_SetRocketClassTurnRateLimit(Handle hPlugin, int iNumParams) { + RocketClassTurnRateLimit[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassElevationRate(Handle hPlugin, int iNumParams) { + return view_as(RocketClassElevationRate[GetNativeCell(1)]); +} + +public any Native_SetRocketClassElevationRate(Handle hPlugin, int iNumParams) { + RocketClassElevationRate[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassElevationLimit(Handle hPlugin, int iNumParams) { + return view_as(RocketClassElevationLimit[GetNativeCell(1)]); +} + +public any Native_SetRocketClassElevationLimit(Handle hPlugin, int iNumParams) { + RocketClassElevationLimit[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassRocketsModifier(Handle hPlugin, int iNumParams) { + return view_as(RocketClassRocketsModifier[GetNativeCell(1)]); +} + +public any Native_SetRocketClassRocketsModifier(Handle hPlugin, int iNumParams) { + RocketClassRocketsModifier[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassPlayerModifier(Handle hPlugin, int iNumParams) { + return view_as(RocketClassPlayerModifier[GetNativeCell(1)]); +} + +public any Native_SetRocketClassPlayerModifier(Handle hPlugin, int iNumParams) { + RocketClassPlayerModifier[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassControlDelay(Handle hPlugin, int iNumParams) { + return view_as(RocketClassControlDelay[GetNativeCell(1)]); +} + +public any Native_SetRocketClassControlDelay(Handle hPlugin, int iNumParams) { + RocketClassControlDelay[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + + +public any Native_GetRocketClassMaxBounces(Handle hPlugin, int iNumParams) { + return RocketClassMaxBounces[GetNativeCell(1)]; +} + +public any Native_SetRocketClassMaxBounces(Handle hPlugin, int iNumParams) { + RocketClassMaxBounces[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassName(Handle hPlugin, int iNumParams) { + SetNativeString(2, RocketClassName[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetRocketClassName(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(RocketClassName[iClass], sizeof(RocketClassName[]), strBuffer); + return 0; +} + +public any Native_GetRocketClassLongName(Handle hPlugin, int iNumParams) { + SetNativeString(2, RocketClassLongName[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetRocketClassLongName(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(RocketClassLongName[iClass], sizeof(RocketClassLongName[]), strBuffer); + return 0; +} + +public any Native_GetRocketClassModel(Handle hPlugin, int iNumParams) { + SetNativeString(2, RocketClassModel[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetRocketClassModel(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(RocketClassModel[iClass], sizeof(RocketClassModel[]), strBuffer); + return 0; +} + +public any Native_GetRocketClassBeepInterval(Handle hPlugin, int iNumParams) { + return view_as(RocketClassBeepInterval[GetNativeCell(1)]); +} + +public any Native_SetRocketClassBeepInterval(Handle hPlugin, int iNumParams) { + RocketClassBeepInterval[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassSpawnSound(Handle hPlugin, int iNumParams) { + SetNativeString(2, RocketClassSpawnSound[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetRocketClassSpawnSound(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(RocketClassSpawnSound[iClass], sizeof(RocketClassSpawnSound[]), strBuffer); + return 0; +} + +public any Native_GetRocketClassBeepSound(Handle hPlugin, int iNumParams) { + SetNativeString(2, RocketClassBeepSound[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetRocketClassBeepSound(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(RocketClassBeepSound[iClass], sizeof(RocketClassBeepSound[]), strBuffer); + return 0; +} + +public any Native_GetRocketClassAlertSound(Handle hPlugin, int iNumParams) { + SetNativeString(2, RocketClassAlertSound[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetRocketClassAlertSound(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(RocketClassAlertSound[iClass], sizeof(RocketClassAlertSound[]), strBuffer); + return 0; +} + +public any Native_GetRocketClassCritChance(Handle hPlugin, int iNumParams) { + return view_as(RocketClassCritChance[GetNativeCell(1)]); +} + +public any Native_SetRocketClassCritChance(Handle hPlugin, int iNumParams) { + RocketClassCritChance[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassTargetWeight(Handle hPlugin, int iNumParams) { + return view_as(RocketClassTargetWeight[GetNativeCell(1)]); +} + +public any Native_SetRocketClassTargetWeight(Handle hPlugin, int iNumParams) { + RocketClassTargetWeight[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassBounceScale(Handle hPlugin, int iNumParams) { + return view_as(RocketClassBounceScale[GetNativeCell(1)]); +} + +public any Native_SetRocketClassBounceScale(Handle hPlugin, int iNumParams) { + RocketClassBounceScale[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +// ********************************************************************************* +// NATIVES - SPAWNER GETTERS/SETTERS +// ********************************************************************************* + +public any Native_GetSpawnersName(Handle hPlugin, int iNumParams) { + SetNativeString(2, SpawnersName[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public any Native_SetSpawnersName(Handle hPlugin, int iNumParams) { + int iSpawnerClass = GetNativeCell(1); + int iMaxLen; + GetNativeStringLength(2, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(2, strBuffer, iMaxLen + 1); + strcopy(SpawnersName[iSpawnerClass], sizeof(SpawnersName[]), strBuffer); + return 0; +} + +public any Native_GetSpawnersMaxRockets(Handle hPlugin, int iNumParams) { + return SpawnersMaxRockets[GetNativeCell(1)]; +} + +public any Native_SetSpawnersMaxRockets(Handle hPlugin, int iNumParams) { + SpawnersMaxRockets[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetSpawnersInterval(Handle hPlugin, int iNumParams) { + return view_as(SpawnersInterval[GetNativeCell(1)]); +} + +public any Native_SetSpawnersInterval(Handle hPlugin, int iNumParams) { + SpawnersInterval[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetSpawnersChancesTable(Handle hPlugin, int iNumParams) { + return CloneHandle(SpawnersChancesTable[GetNativeCell(1)], hPlugin); +} + +public any Native_SetSpawnersChancesTable(Handle hPlugin, int iNumParams) { + int iSpawnerClass = GetNativeCell(1); + ArrayList hTable = view_as(GetNativeCell(2)); + ArrayList hClonedTable = view_as(CloneHandle(hTable, GetMyHandle())); + delete SpawnersChancesTable[iSpawnerClass]; + SpawnersChancesTable[iSpawnerClass] = hClonedTable; + return 0; +} + +public any Native_GetSpawnersCount(Handle hPlugin, int iNumParams) { + return SpawnersCount; +} + +public any Native_SetSpawnersCount(Handle hPlugin, int iNumParams) { + SpawnersCount = GetNativeCell(1); + return 0; +} + +// ********************************************************************************* +// NATIVES - SPAWN POINT GETTERS/SETTERS +// ********************************************************************************* + +public any Native_GetCurrentRedSpawn(Handle hPlugin, int iNumParams) { + return CurrentRedSpawn; +} + +public any Native_SetCurrentRedSpawn(Handle hPlugin, int iNumParams) { + CurrentRedSpawn = GetNativeCell(1); + return 0; +} + +public any Native_GetSpawnPointsRedCount(Handle hPlugin, int iNumParams) { + return SpawnPointsRedCount; +} + +public any Native_SetSpawnPointsRedCount(Handle hPlugin, int iNumParams) { + SpawnPointsRedCount = GetNativeCell(1); + return 0; +} + +public any Native_GetSpawnPointsRedClass(Handle hPlugin, int iNumParams) { + return SpawnPointsRedClass[GetNativeCell(1)]; +} + +public any Native_SetSpawnPointsRedClass(Handle hPlugin, int iNumParams) { + SpawnPointsRedClass[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetSpawnPointsRedEntity(Handle hPlugin, int iNumParams) { + return SpawnPointsRedEntity[GetNativeCell(1)]; +} + +public any Native_SetSpawnPointsRedEntity(Handle hPlugin, int iNumParams) { + SpawnPointsRedEntity[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetCurrentBluSpawn(Handle hPlugin, int iNumParams) { + return CurrentBluSpawn; +} + +public any Native_SetCurrentBluSpawn(Handle hPlugin, int iNumParams) { + CurrentBluSpawn = GetNativeCell(1); + return 0; +} + +public any Native_GetSpawnPointsBluCount(Handle hPlugin, int iNumParams) { + return SpawnPointsBluCount; +} + +public any Native_SetSpawnPointsBluCount(Handle hPlugin, int iNumParams) { + SpawnPointsBluCount = GetNativeCell(1); + return 0; +} + +public any Native_GetSpawnPointsBluClass(Handle hPlugin, int iNumParams) { + return SpawnPointsBluClass[GetNativeCell(1)]; +} + +public any Native_SetSpawnPointsBluClass(Handle hPlugin, int iNumParams) { + SpawnPointsBluClass[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetSpawnPointsBluEntity(Handle hPlugin, int iNumParams) { + return SpawnPointsBluEntity[GetNativeCell(1)]; +} + +public any Native_SetSpawnPointsBluEntity(Handle hPlugin, int iNumParams) { + SpawnPointsBluEntity[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +// ********************************************************************************* +// NATIVES - GAMEPLAY GETTERS/SETTERS +// ********************************************************************************* + +public any Native_GetRoundStarted(Handle hPlugin, int iNumParams) { + return RoundStarted; +} + +public any Native_GetRoundCount(Handle hPlugin, int iNumParams) { + return RoundCount; +} + +public any Native_GetRocketsFired(Handle hPlugin, int iNumParams) { + return RocketsFired; +} + +public any Native_GetNextSpawnTime(Handle hPlugin, int iNumParams) { + return view_as(NextSpawnTime); +} + +public any Native_SetNextSpawnTime(Handle hPlugin, int iNumParams) { + NextSpawnTime = GetNativeCell(1); + return 0; +} + +public any Native_GetLastDeadTeam(Handle hPlugin, int iNumParams) { + return LastDeadTeam; +} + +public any Native_GetLastDeadClient(Handle hPlugin, int iNumParams) { + return LastDeadClient; +} + +public any Native_GetLastStealer(Handle hPlugin, int iNumParams) { + return LastStealer; +} + +public any Native_GetStealInfo(Handle hPlugin, int iNumParams) { + int iClient = GetNativeCell(1); + if (iClient < 1 || iClient > MaxClients) { + return ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", iClient); + } + SetNativeCellRef(2, StealInfo[iClient].stoleRocket); + SetNativeCellRef(3, StealInfo[iClient].rocketsStolen); + return 0; +} + +public any Native_SetStealInfo(Handle hPlugin, int iNumParams) { + int iClient = GetNativeCell(1); + if (iClient < 1 || iClient > MaxClients) { + return ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index %d", iClient); + } + StealInfo[iClient].stoleRocket = view_as(GetNativeCell(2)); + StealInfo[iClient].rocketsStolen = GetNativeCell(3); + return 0; +} + +// ********************************************************************************* +// NATIVES - DATAPACK-BASED GETTERS/SETTERS +// ********************************************************************************* + +public any Native_GetRocketClassCmdsOnSpawn(Handle hPlugin, int iNumParams) { + return CloneHandle(RocketClassCmdsOnSpawn[GetNativeCell(1)], hPlugin); +} + +public any Native_SetRocketClassCmdsOnSpawn(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + DataPack hCmds = view_as(GetNativeCell(2)); + DataPack hClonedPack = view_as(CloneHandle(hCmds, GetMyHandle())); + delete RocketClassCmdsOnSpawn[iClass]; + RocketClassCmdsOnSpawn[iClass] = hClonedPack; + return 0; +} + +public any Native_GetRocketClassCmdsOnDeflect(Handle hPlugin, int iNumParams) { + return CloneHandle(RocketClassCmdsOnDeflect[GetNativeCell(1)], hPlugin); +} + +public any Native_SetRocketClassCmdsOnDeflect(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + DataPack hCmds = view_as(GetNativeCell(2)); + DataPack hClonedPack = view_as(CloneHandle(hCmds, GetMyHandle())); + delete RocketClassCmdsOnDeflect[iClass]; + RocketClassCmdsOnDeflect[iClass] = hClonedPack; + return 0; +} + +public any Native_GetRocketClassCmdsOnKill(Handle hPlugin, int iNumParams) { + return CloneHandle(RocketClassCmdsOnKill[GetNativeCell(1)], hPlugin); +} + +public any Native_SetRocketClassCmdsOnKill(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + DataPack hCmds = view_as(GetNativeCell(2)); + DataPack hClonedPack = view_as(CloneHandle(hCmds, GetMyHandle())); + delete RocketClassCmdsOnKill[iClass]; + RocketClassCmdsOnKill[iClass] = hClonedPack; + return 0; +} + +public any Native_GetRocketClassCmdsOnExplode(Handle hPlugin, int iNumParams) { + return CloneHandle(RocketClassCmdsOnExplode[GetNativeCell(1)], hPlugin); +} + +public any Native_SetRocketClassCmdsOnExplode(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + DataPack hCmds = view_as(GetNativeCell(2)); + DataPack hClonedPack = view_as(CloneHandle(hCmds, GetMyHandle())); + delete RocketClassCmdsOnExplode[iClass]; + RocketClassCmdsOnExplode[iClass] = hClonedPack; + return 0; +} + +public any Native_GetRocketClassCmdsOnNoTarget(Handle hPlugin, int iNumParams) { + return CloneHandle(RocketClassCmdsOnNoTarget[GetNativeCell(1)], hPlugin); +} + +public any Native_SetRocketClassCmdsOnNoTarget(Handle hPlugin, int iNumParams) { + int iClass = GetNativeCell(1); + DataPack hCmds = view_as(GetNativeCell(2)); + DataPack hClonedPack = view_as(CloneHandle(hCmds, GetMyHandle())); + delete RocketClassCmdsOnNoTarget[iClass]; + RocketClassCmdsOnNoTarget[iClass] = hClonedPack; + return 0; +} + +// ********************************************************************************* +// NATIVES - ACTION FUNCTIONS +// ********************************************************************************* + +public any Native_CreateRocket(Handle hPlugin, int iNumParams) { + CreateRocket(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3), GetNativeCell(4)); + return 0; +} + +public any Native_DestroyRocket(Handle hPlugin, int iNumParams) { + DestroyRocket(GetNativeCell(1)); + return 0; +} + +public any Native_DestroyRockets(Handle hPlugin, int iNumParams) { + DestroyRockets(); + return 0; +} + +public any Native_DestroyRocketClasses(Handle hPlugin, int iNumParams) { + DestroyRocketClasses(); + return 0; +} + +public any Native_DestroySpawners(Handle hPlugin, int iNumParams) { + DestroySpawners(); + return 0; +} + +public any Native_ParseConfigurations(Handle hPlugin, int iNumParams) { + int iMaxLen; + GetNativeStringLength(1, iMaxLen); + char[] strBuffer = new char[iMaxLen + 1]; + GetNativeString(1, strBuffer, iMaxLen + 1); + ParseConfigurations(strBuffer); + return 0; +} + +public any Native_PopulateSpawnPoints(Handle hPlugin, int iNumParams) { + PopulateSpawnPoints(); + return 0; +} + +public any Native_HomingRocketThink(Handle hPlugin, int iNumParams) { + HomingRocketThink(GetNativeCell(1)); + return 0; +} + +public any Native_RocketLegacyThink(Handle hPlugin, int iNumParams) { + RocketLegacyThink(GetNativeCell(1)); + return 0; +} + +// ********************************************************************************* +// NATIVES - PRESETS +// ********************************************************************************* + +public any Native_GetPresetCount(Handle hPlugin, int iNumParams) { + return PresetCount; +} + +public any Native_GetPresetName(Handle hPlugin, int iNumParams) { + int iPreset = GetNativeCell(1); + int iMaxLen = GetNativeCell(3); + SetNativeString(2, PresetName[iPreset], iMaxLen); + return 0; +} + +public any Native_ApplyPreset(Handle hPlugin, int iNumParams) { + int iPreset = GetNativeCell(1); + if (iPreset < 0 || iPreset >= PresetCount) return false; + + // Find the rocket class index by short name + int iTargetClass = -1; + for (int i = 0; i < RocketClassCount; i++) + { + if (StrEqual(RocketClassName[i], PresetRocketClass[iPreset], false)) + { + iTargetClass = i; + break; + } + } + if (iTargetClass == -1) return false; + + // Destroy active rockets before changing settings + DestroyRockets(); + + // Apply to all spawners + for (int i = 0; i < SpawnersCount; i++) + { + SpawnersMaxRockets[i] = PresetMaxRockets[iPreset]; + + if (PresetSpawnInterval[iPreset] > 0.0) + { + SpawnersInterval[i] = PresetSpawnInterval[iPreset]; + } + + // Set chances table: 100% target class, 0% everything else + ArrayList hTable = SpawnersChancesTable[i]; + for (int j = 0; j < hTable.Length; j++) + { + hTable.Set(j, (j == iTargetClass) ? 100 : 0); + } + } + + return true; +} \ No newline at end of file diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc new file mode 100644 index 00000000..ca08f4a4 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc @@ -0,0 +1,746 @@ +#if defined _dodgeball_rockets_included + #endinput +#endif +#define _dodgeball_rockets_included + +/** + * All logic directly related to creating, managing, and controlling rockets. + */ + +/** + * Creates and spawns a new rocket from a spawner. + * + * @param iSpawnerEntity The spawner entity to spawn from. + * @param iSpawnerClass The spawner class index (determines spawn chances). + * @param iTeam The team the rocket belongs to (TFTeam_Red or TFTeam_Blue). + * @param iClass Optional rocket class (-1 for random from spawner chances). + */ +void CreateRocket(int iSpawnerEntity, int iSpawnerClass, int iTeam, int iClass = -1) +{ + int iIndex = FindFreeRocketSlot(); + if (iIndex == -1) return; + + iClass = iClass == -1 ? GetRandomRocketClass(iSpawnerClass) : iClass; + RocketFlags iFlags = RocketClassFlags[iClass]; + + int iClassRef = iClass; + RocketFlags iFlagsRef = iFlags; + Action aResult = Forward_OnRocketCreatedPre(iIndex, iClassRef, iFlagsRef); + + if (aResult == Plugin_Stop || aResult == Plugin_Handled) + { + return; + } + else if (aResult == Plugin_Changed) + { + iClass = iClassRef; + iFlags = iFlagsRef; + } + + int iEntity = CreateEntityByName(TestFlags(iFlags, RocketFlag_IsAnimated) ? "tf_projectile_sentryrocket" : "tf_projectile_rocket"); + if (iEntity == -1) return; + + float fPosition[3], fAngles[3], fDirection[3]; + GetEntPropVector(iSpawnerEntity, Prop_Send, "m_vecOrigin", fPosition); + GetEntPropVector(iSpawnerEntity, Prop_Send, "m_angRotation", fAngles); + GetAngleVectors(fAngles, fDirection, NULL_VECTOR, NULL_VECTOR); + + SetEntProp(iEntity, Prop_Send, "m_bCritical", (GetURandomFloatRange(0.0, 100.0) <= RocketClassCritChance[iClass]) ? 1 : 0, 1); + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", (TestFlags(iFlags, RocketFlag_IsNeutral)) ? 1 : iTeam, 1); + SetEntProp(iEntity, Prop_Send, "m_iDeflected", 0); + TeleportEntity(iEntity, fPosition, fAngles, view_as({0.0, 0.0, 0.0})); + + int iTargetTeam = (TestFlags(iFlags, RocketFlag_IsNeutral)) ? 0 : GetAnalogueTeam(iTeam); + int iTarget = SelectTarget(iTargetTeam); + int iRocketOwner = SelectTarget(GetAnalogueTeam(GetClientTeam(iTarget))); + + SetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity", iRocketOwner); + int iWeapon = GetPlayerWeaponSlot(iRocketOwner, TFWeaponSlot_Primary); + SetEntPropEnt(iEntity, Prop_Send, "m_hOriginalLauncher", iWeapon == -1 ? iEntity : iWeapon); + SetEntPropEnt(iEntity, Prop_Send, "m_hLauncher", iWeapon == -1 ? iEntity : iWeapon); + + float fModifier = CalculateModifier(iClass, 0); + RocketIsValid[iIndex] = true; + RocketInstanceFlags[iIndex] = iFlags; + RocketEntity[iIndex] = EntIndexToEntRef(iEntity); + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + RocketClass[iIndex] = iClass; + RocketDeflections[iIndex] = 0; + RocketEventDeflections[iIndex] = 0; + RocketBounces[iIndex] = 0; + RocketHomingPaused[iIndex] = false; + RocketIsDragPause[iIndex] = false; + RocketDragPauseEnd[iIndex] = 0.0; + RocketLastDeflectionTime[iIndex] = GetGameTime(); + RocketLastBeepTime[iIndex] = GetGameTime(); + RocketSpeed[iIndex] = CalculateRocketSpeed(iClass, fModifier); + RocketMphSpeed[iIndex] = RocketSpeed[iIndex] * HAMMER_TO_MPH; + Internal_SetRocketState(iIndex, RocketState_None); + + CopyVectors(fDirection, RocketDirection[iIndex]); + SetEntDataFloat(iEntity, DamageOffset, CalculateRocketDamage(iClass, fModifier), true); + DispatchSpawn(iEntity); + + if (TestFlags(iFlags, RocketFlag_CustomModel)) + { + SetEntityModel(iEntity, RocketClassModel[iClass]); + UpdateRocketSkin(iEntity, iTeam, TestFlags(iFlags, RocketFlag_IsNeutral)); + } + + if (TestFlags(iFlags, RocketFlag_OnSpawnCmd)) + { + ExecuteCommands(RocketClassCmdsOnSpawn[iClass], iClass, iEntity, 0, iTarget, LastDeadClient, RocketSpeed[iIndex], 0, RocketMphSpeed[iIndex]); + } + + EmitRocketSound(RocketSound_Spawn, iClass, iEntity, iTarget, iFlags); + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); + + RocketCount++; + RocketsFired++; + LastSpawnTime[iIndex] = GetGameTime(); + NextSpawnTime = GetGameTime() + SpawnersInterval[iSpawnerClass]; + + SDKHook(iEntity, SDKHook_StartTouch, OnStartTouch); + Forward_OnRocketCreated(iIndex, iEntity); +} + +/** + * Destroys a rocket by its internal index. + * + * @param iIndex The rocket's internal index. + */ +void DestroyRocket(int iIndex) +{ + if (!IsValidRocket(iIndex)) return; + + int iEntity = EntRefToEntIndex(RocketEntity[iIndex]); + if (iEntity != INVALID_ENT_REFERENCE) + { + RemoveEntity(iEntity); + } + RocketIsValid[iIndex] = false; + RocketCount--; +} + +/** + * Destroys all active rockets. + */ +void DestroyRockets() +{ + for (int iIndex = 0; iIndex < MAX_ROCKETS; iIndex++) + { + DestroyRocket(iIndex); + } + RocketCount = 0; +} + +/** + * Checks if a rocket index is valid and the rocket exists. + * + * @param iIndex The rocket's internal index. + * @return True if valid, false otherwise. + */ +bool IsValidRocket(int iIndex) +{ + if (!((iIndex >= 0) && RocketIsValid[iIndex])) return false; + + if (EntRefToEntIndex(RocketEntity[iIndex]) != -1) return true; + + RocketIsValid[iIndex] = false; + RocketCount--; + + return false; +} + +/** + * Finds the next valid rocket after the given index. + * + * @param iIndex Starting index to search from. + * @return Next valid rocket index, or -1 if none found. + */ +int FindNextValidRocket(int iIndex) +{ + for (int iCurrent = iIndex + 1; iCurrent < MAX_ROCKETS; iCurrent++) + { + if (IsValidRocket(iCurrent)) return iCurrent; + } + return -1; +} + +/** + * Finds a free slot in the rocket array. + * + * @return Free rocket index, or -1 if array is full. + */ +int FindFreeRocketSlot() +{ + for (int iCurrent = 0; iCurrent < MAX_ROCKETS; iCurrent++) + { + if (!IsValidRocket(iCurrent)) return iCurrent; + } + + return -1; +} + +/** + * Finds a rocket's internal index by its entity index. + * + * @param iEntity The rocket's entity index. + * @return Internal rocket index, or -1 if not found. + */ +int FindRocketByEntity(int iEntity) +{ + int iIndex = -1; + while ((iIndex = FindNextValidRocket(iIndex)) != -1) + { + if (EntRefToEntIndex(RocketEntity[iIndex]) == iEntity) return iIndex; + } + return -1; +} + +/** + * Processes a rocket deflection: reads the deflector's aim, selects a new target, + * recalculates speed/damage, checks for steals, and fires forwards/commands. + * Shared by HomingRocketThink, SharedRocketThink, and RocketLegacyThink. + * + * @param iIndex Rocket internal index. + * @param iEntity Rocket entity index. + * @param iClass Rocket class index. + * @param iFlags Rocket instance flags. + * @param iTeam Rocket's current team. + * @param iTargetTeam Team to select new target from. + * @param iDeflectionCount Current event deflection count. + * @param fModifier Speed/damage modifier for current deflection count. + * @param bClearDragging True to clear RocketState_Dragging (homing/shared). + * False for legacy (never enters drag state). + * @return False if Forward_OnRocketDeflectPre blocked the deflection. + */ +bool ProcessDeflection(int iIndex, int iEntity, int iClass, RocketFlags iFlags, + int iTeam, int iTargetTeam, int iDeflectionCount, float fModifier, + bool bClearDragging) +{ + int iClient = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + + if (bClearDragging) + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~(RocketState_Dragging | RocketState_Stolen))); + else + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~RocketState_Stolen)); + + if (iClient >= 1) + { + float fViewAngles[3], fDirection[3]; + GetClientEyeAngles(iClient, fViewAngles); + GetAngleVectors(fViewAngles, fDirection, NULL_VECTOR, NULL_VECTOR); + CopyVectors(fDirection, RocketDirection[iIndex]); + UpdateRocketSkin(iEntity, iTeam, TestFlags(iFlags, RocketFlag_IsNeutral)); + + if (!(iFlags & RocketFlag_CanBeStolen)) + { + CheckStolenRocket(iClient, iIndex); + } + } + + int iTarget = SelectTarget(iTargetTeam, iIndex); + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + RocketDeflections[iIndex] = iDeflectionCount; + RocketLastDeflectionTime[iIndex] = GetGameTime(); + RocketSpeed[iIndex] = CalculateRocketSpeed(iClass, fModifier); + SetEntDataFloat(iEntity, DamageOffset, CalculateRocketDamage(iClass, fModifier), true); + + if (TestFlags(iFlags, RocketFlag_ElevateOnDeflect)) RocketInstanceFlags[iIndex] |= RocketFlag_Elevating; + + if ((iFlags & RocketFlag_IsSpeedLimited) && (RocketSpeed[iIndex] >= RocketClassSpeedLimit[iClass])) + { + RocketSpeed[iIndex] = RocketClassSpeedLimit[iClass]; + } + + RocketMphSpeed[iIndex] = RocketSpeed[iIndex] * HAMMER_TO_MPH; + + if (CvarStealPreventionDamage.BoolValue && (RocketInstanceState[iIndex] & RocketState_Stolen)) + { + SetEntDataFloat(iEntity, DamageOffset, 0.0, true); + } + + if (iFlags & RocketFlag_TeamlessHits) + { + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); + } + + int iTargetRef = iTarget; + Action aResult = Forward_OnRocketDeflectPre(iIndex, iEntity, iClient, iTargetRef); + + if (aResult == Plugin_Stop || aResult == Plugin_Handled) + { + return false; + } + else if (aResult == Plugin_Changed) + { + iTarget = iTargetRef; + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + } + + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); + if (TestFlags(iFlags, RocketFlag_OnDeflectCmd)) + { + ExecuteCommands(RocketClassCmdsOnDeflect[iClass], iClass, iEntity, iClient, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); + } + Forward_OnRocketDeflect(iIndex, iEntity, iClient); + + return true; +} + +/** + * Main think function for homing rockets. Handles tracking, dragging, + * deflection, target switching, and all per-frame rocket behaviour. + * Called from the logic timer in dodgeball_core.inc (~10Hz due to SourceMod's 0.1s min timer). + * + * @param iIndex The rocket's internal index. + */ +void HomingRocketThink(int iIndex) +{ + int iEntity = EntRefToEntIndex(RocketEntity[iIndex]); + int iClass = RocketClass[iIndex]; + RocketFlags iFlags = RocketInstanceFlags[iIndex]; + int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); + int iTeam = GetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1); + int iTargetTeam = (TestFlags(iFlags, RocketFlag_IsNeutral)) ? 0 : GetAnalogueTeam(iTeam); + int iDeflectionCount = RocketEventDeflections[iIndex]; + float fModifier = CalculateModifier(iClass, iDeflectionCount); + + // --- Deflection detected: pause homing, defer aim reading to per-frame check --- + if ((iDeflectionCount > RocketDeflections[iIndex]) && !(RocketInstanceState[iIndex] & RocketState_Dragging)) + { + RocketHomingPaused[iIndex] = true; + RocketIsDragPause[iIndex] = true; + + // Snap drag pause to tick boundary so it ends on a deterministic frame. + // GetTickInterval() is the canonical frame duration from SM 1.12 (timers.inc). + float tickInterval = GetTickInterval(); + int pauseTicks = RoundToNearest(RocketClassDragPauseDuration[iClass] / tickInterval); + RocketDragPauseEnd[iIndex] = GetGameTime() + (pauseTicks * tickInterval); + + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] | (RocketState_CanDrag | RocketState_Dragging))); + } + + // --- Drag pause expired: process deflection per-frame (allows sub-100ms drag) --- + if (RocketHomingPaused[iIndex] && RocketIsDragPause[iIndex] && GetGameTime() >= RocketDragPauseEnd[iIndex]) + { + if (iDeflectionCount > RocketDeflections[iIndex]) + { + if (!ProcessDeflection(iIndex, iEntity, iClass, iFlags, iTeam, iTargetTeam, iDeflectionCount, fModifier, true)) + { + RocketHomingPaused[iIndex] = false; + RocketIsDragPause[iIndex] = false; + return; + } + } + + // Apply parameters and re-enable smooth homing + ApplyRocketParameters(iIndex); + RocketHomingPaused[iIndex] = false; + RocketIsDragPause[iIndex] = false; + } + + // --- Normal homing: lerp toward target (only when not paused) --- + if (!RocketHomingPaused[iIndex]) + { + if (!IsValidClient(iTarget, true)) + { + int iOwner = iTarget; + iTarget = SelectTarget(iTargetTeam, iIndex); + + if (!IsValidClient(iTarget, true)) return; + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); + + if (TestFlags(iFlags, RocketFlag_OnNoTargetCmd)) + { + ExecuteCommands(RocketClassCmdsOnNoTarget[iClass], iClass, iEntity, iOwner, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); + } + + if (CvarNoTargetRedirectDamage.BoolValue) + { + SetEntDataFloat(iEntity, DamageOffset, 0.0, true); + } + Forward_OnRocketNoTarget(iIndex, iTarget, iOwner); + } + else + { + if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) + { + // Scale turn rate from "per TICK_BASE_INTERVAL" to "per frame" using GetTickInterval(). + float fTickScale = GetTickInterval() / TICK_BASE_INTERVAL; + float fTurnrate = CalculateRocketTurnRate(iClass, fModifier) * fTickScale; + float fDirectionToTarget[3]; CalculateDirectionToClient(iEntity, iTarget, fDirectionToTarget); + + if (RocketInstanceFlags[iIndex] & RocketFlag_Elevating) + { + fDirectionToTarget[2] = RocketDirection[iIndex][2]; + } + + if ((RocketInstanceFlags[iIndex] & RocketFlag_IsTRLimited) && (fTurnrate >= RocketClassTurnRateLimit[iClass] * fTickScale)) + { + fTurnrate = RocketClassTurnRateLimit[iClass] * fTickScale; + } + LerpVectors(RocketDirection[iIndex], fDirectionToTarget, RocketDirection[iIndex], fTurnrate); + } + } + + // --- Smooth elevation (OnGameFrame, frame-rate independent) --- + if (UseSmoothElevation && (RocketInstanceFlags[iIndex] & RocketFlag_Elevating)) + { + if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) + { + float fElevRate = RocketClassElevationRate[iClass] * (GetTickInterval() / TICK_BASE_INTERVAL); + if (RocketDirection[iIndex][2] < RocketClassElevationLimit[iClass]) + { + RocketDirection[iIndex][2] = FMin(RocketDirection[iIndex][2] + fElevRate, RocketClassElevationLimit[iClass]); + } + else + { + RocketInstanceFlags[iIndex] &= ~RocketFlag_Elevating; + } + } + } + + if (!(RocketInstanceState[iIndex] & RocketState_Bouncing)) + { + ApplyRocketParameters(iIndex); + } + } +} + +/** + * Handles logic that is shared between all rocket behaviours, like sounds and delay checks. + * This is called from the logic timer in dodgeball_core.inc + */ +void SharedRocketThink(int iIndex) +{ + int iEntity = EntRefToEntIndex(RocketEntity[iIndex]); + int iClass = RocketClass[iIndex]; + RocketFlags iFlags = RocketInstanceFlags[iIndex]; + int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); + int iTeam = GetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1); + int iTargetTeam = (TestFlags(iFlags, RocketFlag_IsNeutral)) ? 0 : GetAnalogueTeam(iTeam); + int iDeflectionCount = RocketEventDeflections[iIndex]; + float fModifier = CalculateModifier(iClass, iDeflectionCount); + + // --- Handle paused state: bounce unpauses are handled here (timer-based) --- + // Drag unpauses are handled per-frame in HomingRocketThink for sub-100ms precision. + if (RocketHomingPaused[iIndex] && !RocketIsDragPause[iIndex]) + { + // Process pending deflection on the logic timer (bounce unpause) + if (iDeflectionCount > RocketDeflections[iIndex]) + { + if (!ProcessDeflection(iIndex, iEntity, iClass, iFlags, iTeam, iTargetTeam, iDeflectionCount, fModifier, true)) + { + RocketHomingPaused[iIndex] = false; + return; + } + } + + // Apply parameters once on unpause and re-enable smooth homing + ApplyRocketParameters(iIndex); + RocketHomingPaused[iIndex] = false; + } + + // --- Elevation (runs at logic timer rate when smooth elevation is off) --- + if (!(iDeflectionCount > RocketDeflections[iIndex])) + { + if (((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) && + (RocketInstanceFlags[iIndex] & RocketFlag_Elevating) && !UseSmoothElevation) + { + if (RocketDirection[iIndex][2] < RocketClassElevationLimit[iClass]) + { + RocketDirection[iIndex][2] = FMin(RocketDirection[iIndex][2] + RocketClassElevationRate[iClass], RocketClassElevationLimit[iClass]); + } + else + { + RocketInstanceFlags[iIndex] &= ~RocketFlag_Elevating; + } + } + + if ((GetGameTime() - RocketLastBeepTime[iIndex]) >= RocketClassBeepInterval[iClass]) + { + EmitRocketSound(RocketSound_Beep, iClass, iEntity, iTarget, iFlags); + RocketLastBeepTime[iIndex] = GetGameTime(); + } + + if (CvarDelayPrevention.BoolValue) + { + CheckRoundDelays(iIndex); + } + } + + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~RocketState_Bouncing)); +} + +/** + * Think function for legacy homing rockets. Uses instant direction changes + * instead of smooth tracking. Called from the logic timer. + * + * @param iIndex The rocket's internal index. + */ +void RocketLegacyThink(int iIndex) +{ + int iEntity = EntRefToEntIndex(RocketEntity[iIndex]); + int iClass = RocketClass[iIndex]; + RocketFlags iFlags = RocketInstanceFlags[iIndex]; + int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); + int iTeam = GetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1); + int iTargetTeam = (TestFlags(iFlags, RocketFlag_IsNeutral)) ? 0 : GetAnalogueTeam(iTeam); + int iDeflectionCount = RocketEventDeflections[iIndex]; + float fModifier = CalculateModifier(iClass, iDeflectionCount); + + if (!IsValidClient(iTarget, true)) + { + int iOwner = iTarget; + iTarget = SelectTarget(iTargetTeam, iIndex); + + if (!IsValidClient(iTarget, true)) return; + + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); + + if (TestFlags(iFlags, RocketFlag_OnNoTargetCmd)) + { + ExecuteCommands(RocketClassCmdsOnNoTarget[iClass], iClass, iEntity, iOwner, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); + } + + if (CvarNoTargetRedirectDamage.BoolValue) + { + SetEntDataFloat(iEntity, DamageOffset, 0.0, true); + } + Forward_OnRocketNoTarget(iIndex, iTarget, iOwner); + } + else if (iDeflectionCount > RocketDeflections[iIndex]) + { + if (!ProcessDeflection(iIndex, iEntity, iClass, iFlags, iTeam, iTargetTeam, iDeflectionCount, fModifier, false)) + { + return; + } + } + else + { + if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) + { + float fTurnrate = CalculateRocketTurnRate(iClass, fModifier); + float fDirectionToTarget[3]; + CalculateDirectionToClient(iEntity, iTarget, fDirectionToTarget); + + if (RocketInstanceFlags[iIndex] & RocketFlag_Elevating) + { + if (RocketDirection[iIndex][2] < RocketClassElevationLimit[iClass]) + { + RocketDirection[iIndex][2] = FMin(RocketDirection[iIndex][2] + RocketClassElevationRate[iClass], RocketClassElevationLimit[iClass]); + fDirectionToTarget[2] = RocketDirection[iIndex][2]; + } + else + { + RocketInstanceFlags[iIndex] &= ~RocketFlag_Elevating; + } + } + + if ((RocketInstanceFlags[iIndex] & RocketFlag_IsTRLimited) && (fTurnrate >= RocketClassTurnRateLimit[iClass])) + { + fTurnrate = RocketClassTurnRateLimit[iClass]; + } + LerpVectors(RocketDirection[iIndex], fDirectionToTarget, RocketDirection[iIndex], fTurnrate); + } + } + ApplyRocketParameters(iIndex); +} + +/** Calculates the modifier based on deflections, rockets fired, and player count. */ +float CalculateModifier(int iClass, int iDeflections) +{ + return iDeflections + + (RocketsFired * RocketClassRocketsModifier[iClass]) + + (PlayerCount * RocketClassPlayerModifier[iClass]); +} + +/** Calculates rocket damage based on class settings and modifier. */ +float CalculateRocketDamage(int iClass, float fModifier) +{ + return RocketClassDamage[iClass] + RocketClassDamageIncrement[iClass] * fModifier; +} + +/** Calculates rocket speed based on class settings and modifier. */ +float CalculateRocketSpeed(int iClass, float fModifier) +{ + return RocketClassSpeed[iClass] + RocketClassSpeedIncrement[iClass] * fModifier; +} + +/** Calculates turn rate based on class settings and modifier. */ +float CalculateRocketTurnRate(int iClass, float fModifier) +{ + if (UseOrbitCoefficient && RocketClassOrbitTightness[iClass] > 0.0) + { + // Scale by 0.001 so config values use a friendly 0.1–1.0 range + // (0.3 ≈ default turn rate feel, 0.1 = loose, 1.0 = tight) + return CalculateRocketSpeed(iClass, fModifier) * RocketClassOrbitTightness[iClass] * 0.001; + } + return RocketClassTurnRate[iClass] + RocketClassTurnRateIncrement[iClass] * fModifier; +} + +/** Calculates the normalized direction vector from rocket to client. */ +void CalculateDirectionToClient(int iEntity, int iClient, float fOut[3]) +{ + float fRocketPosition[3]; GetEntPropVector(iEntity, Prop_Send, "m_vecOrigin", fRocketPosition); + GetClientEyePosition(iClient, fOut); + MakeVectorFromPoints(fRocketPosition, fOut, fOut); + NormalizeVector(fOut, fOut); +} + +/** Applies rocket direction and speed to the entity's velocity. */ +void ApplyRocketParameters(int iIndex) +{ + int iEntity = EntRefToEntIndex(RocketEntity[iIndex]); + float fAngles[3]; GetVectorAngles(RocketDirection[iIndex], fAngles); + float fVelocity[3]; CopyVectors(RocketDirection[iIndex], fVelocity); + + ScaleVector(fVelocity, RocketSpeed[iIndex]); + SetEntPropVector(iEntity, Prop_Data, "m_vecAbsVelocity", fVelocity); + SetEntPropVector(iEntity, Prop_Send, "m_angRotation", fAngles); +} + +/** Updates the rocket's skin based on team and neutral status. */ +void UpdateRocketSkin(int iEntity, int iTeam, bool bNeutral) +{ + if (bNeutral) SetEntProp(iEntity, Prop_Send, "m_nSkin", 2); + else SetEntProp(iEntity, Prop_Send, "m_nSkin", (iTeam == view_as(TFTeam_Blue)) ? 0 : 1); +} + +/** Gets a random rocket class based on spawner spawn chances. */ +int GetRandomRocketClass(int iSpawnerClass) +{ + int iRandom = GetURandomIntRange(1, 100); + ArrayList hTable = SpawnersChancesTable[iSpawnerClass]; + int iTableSize = hTable.Length; + int iChancesLower = 0; + int iChancesUpper = 0; + + for (int iEntry = 0; iEntry < iTableSize; iEntry++) + { + iChancesLower += iChancesUpper; + iChancesUpper = iChancesLower + hTable.Get(iEntry); + + if ((iRandom >= iChancesLower) && (iRandom <= iChancesUpper)) + { + return iEntry; + } + } + return 0; +} + +/** + * Emits a rocket sound (spawn, beep, or alert) based on class settings. + * Note: Beep timing is managed by SharedRocketThink - this function only plays sounds. + */ +void EmitRocketSound(RocketSound iSound, int iClass, int iEntity, int iTarget, RocketFlags iFlags) +{ + switch (iSound) + { + case RocketSound_Spawn: + { + if (TestFlags(iFlags, RocketFlag_PlaySpawnSound)) + { + if (TestFlags(iFlags, RocketFlag_CustomSpawnSound)) EmitSoundToAll(RocketClassSpawnSound[iClass], iEntity); + else EmitSoundToAll(SOUND_DEFAULT_SPAWN, iEntity); + } + } + case RocketSound_Beep: + { + // Beep interval timing is handled by SharedRocketThink before calling this function + if (TestFlags(iFlags, RocketFlag_PlayBeepSound)) + { + if (TestFlags(iFlags, RocketFlag_CustomBeepSound)) EmitSoundToAll(RocketClassBeepSound[iClass], iEntity); + else EmitSoundToAll(SOUND_DEFAULT_BEEP, iEntity); + } + } + case RocketSound_Alert: + { + if (TestFlags(iFlags, RocketFlag_PlayAlertSound)) + { + if (TestFlags(iFlags, RocketFlag_CustomAlertSound)) EmitSoundToClient(iTarget, RocketClassAlertSound[iClass]); + else EmitSoundToClient(iTarget, SOUND_DEFAULT_ALERT, _, _, _, _, 0.5); + } + } + } +} + +/** + * Checks if a rocket deflection was a steal and applies penalties. + * Uses guard clauses for clarity and safety. + */ +void CheckStolenRocket(int iClient, int iIndex) +{ + int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); + + // Guard clauses - each condition is clear and prevents potential crashes + if (iTarget == -1) return; // Invalid target entity + if (!IsValidClient(iTarget, true)) return; // Target not valid/alive + if (iTarget == iClient) return; // Can't steal from yourself + if (StealInfo[iClient].stoleRocket) return; // Already processed a steal this round + if (RocketInstanceState[iIndex] & RocketState_Delayed) return; // Delayed rockets exempt from steal check + if (GetEntitiesDistance(iTarget, iClient) <= CvarStealDistance.FloatValue) return; // Too close to target + + // Team check: if enabled, only count as steal if on same team as target + if ((RocketInstanceFlags[iIndex] & RocketFlag_StealTeamCheck) && + (GetClientTeam(iTarget) != GetClientTeam(iClient))) return; + + // All conditions met - this is a steal + StealInfo[iClient].stoleRocket = true; + + if (StealInfo[iClient].rocketsStolen < CvarStealPreventionNumber.IntValue) + { + StealInfo[iClient].rocketsStolen++; + SlapPlayer(iClient, 0, true); + CPrintToChat(iClient, "%t", "DBSteal_Warning_Client", StealInfo[iClient].rocketsStolen, CvarStealPreventionNumber.IntValue); + + if (CvarStealMessage.BoolValue) + { + CSkipNextClient(iClient); + CPrintToChatAll("%t", "DBSteal_Announce_All", iClient, iTarget); + } + StealInfo[iClient].stoleRocket = false; + } + else + { + ForcePlayerSuicide(iClient); + CPrintToChat(iClient, "%t", "DBSteal_Slay_Client"); + if (CvarStealMessage.BoolValue) + { + CSkipNextClient(iClient); + CPrintToChatAll("%t", "DBSteal_Announce_Slay_All", iClient); + } + } + + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] | RocketState_Stolen)); + LastStealer = iClient; + Forward_OnRocketSteal(iIndex, iClient, iTarget, StealInfo[iClient].rocketsStolen); +} + +void CheckRoundDelays(int iIndex) +{ + int iEntity = EntRefToEntIndex(RocketEntity[iIndex]); + int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); + float fTimeToCheck = RocketDeflections[iIndex] == 0 ? LastSpawnTime[iIndex] : RocketLastDeflectionTime[iIndex]; + + if (iTarget == -1 || (GetGameTime() - fTimeToCheck) < CvarDelayPreventionTime.FloatValue) return; + + if (!(RocketInstanceState[iIndex] & RocketState_Delayed)) + { + if (CvarDelayMessage.BoolValue) + { + CPrintToChatAll("%t", "DBDelay_Announce_All", iTarget); + } + EmitSoundToAll(SOUND_DEFAULT_SPEEDUP, iEntity, SNDCHAN_AUTO, SNDLEVEL_GUNFIRE); + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] | RocketState_Delayed)); + Forward_OnRocketDelay(iIndex, iTarget); + } + else + { + RocketSpeed[iIndex] += CvarDelayPreventionSpeedup.FloatValue; + } +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_utilities.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_utilities.inc new file mode 100644 index 00000000..ab9d3f0e --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_utilities.inc @@ -0,0 +1,373 @@ +#if defined _dodgeball_utilities_included + #endinput +#endif +#define _dodgeball_utilities_included + +/** + * Contains general-purpose utility and helper functions for the + * TF2 Dodgeball plugin. + */ + + // Add this new function to the file +stock void StopSoundToAll(int iChannel, const char[] strSound) +{ + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (IsValidClientEx(iClient)) + { + StopSound(iClient, iChannel, strSound); + } + } +} + +// ********************************************************************************* +// VECTOR & MATH UTILITIES +// ********************************************************************************* + +/** + * Copies the contents from one vector to another. + */ +stock void CopyVectors(float fFrom[3], float fTo[3]) +{ + fTo[0] = fFrom[0]; + fTo[1] = fFrom[1]; + fTo[2] = fFrom[2]; +} + +/** + * Calculates the linear interpolation of two vectors. + */ +stock void LerpVectors(float fA[3], float fB[3], float fC[3], float t) +{ + t = (t < 0.0) ? 0.0 : (t > 1.0) ? 1.0 : t; + + fC[0] = fA[0] + (fB[0] - fA[0]) * t; + fC[1] = fA[1] + (fB[1] - fA[1]) * t; + fC[2] = fA[2] + (fB[2] - fA[2]) * t; +} + +/** + * Gets the distance between two entities. + */ +stock float GetEntitiesDistance(int iEnt1, int iEnt2) +{ + float fOrig1[3]; + GetEntPropVector(iEnt1, Prop_Send, "m_vecOrigin", fOrig1); + + float fOrig2[3]; + GetEntPropVector(iEnt2, Prop_Send, "m_vecOrigin", fOrig2); + + return GetVectorDistance(fOrig1, fOrig2); +} + +/** + * Returns the maximum of two float values. + */ +stock float FMax(float a, float b) +{ + return (a > b) ? a : b; +} + +/** + * Returns the minimum of two float values. + */ +stock float FMin(float a, float b) +{ + return (a < b) ? a : b; +} + +/** + * Returns a random integer within a given range (inclusive). + */ +stock int GetURandomIntRange(int iMin, int iMax) +{ + return iMin + (GetURandomInt() % (iMax - iMin + 1)); +} + +/** + * Returns a random float within a given range. + */ +stock float GetURandomFloatRange(float fMin, float fMax) +{ + return fMin + (GetURandomFloat() * (fMax - fMin)); +} + +// ********************************************************************************* +// CLIENT & GAMEPLAY UTILITIES +// ********************************************************************************* + +/** + * Checks if a client index is valid, connected, and optionally alive. + */ +stock bool IsValidClient(int iClient, bool bAlive = false) +{ + return iClient >= 1 && + iClient <= MaxClients && + IsClientInGame(iClient) && + (!bAlive || IsPlayerAlive(iClient)); +} + +/** + * IsValidClient() but without the index range check. + */ +stock bool IsValidClientEx(int iClient, bool bAlive = false) +{ + return IsClientInGame(iClient) && (!bAlive || IsPlayerAlive(iClient)); +} + +/** + * Checks if there are living players on both teams. + */ +stock bool BothTeamsPlaying() +{ + bool bRedFound, bBluFound; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClientEx(iClient, true)) continue; + + int iTeam = GetClientTeam(iClient); + + if (iTeam == view_as(TFTeam_Red)) bRedFound = true; + if (iTeam == view_as(TFTeam_Blue)) bBluFound = true; + } + + return bRedFound && bBluFound; +} + +/** + * Counts the number of players currently alive. + */ +stock int CountAlivePlayers() +{ + int iCount = 0; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (IsValidClientEx(iClient, true)) iCount++; + } + + return iCount; +} + +/** + * Gets the opposing team. + */ +stock int GetAnalogueTeam(int iTeam) +{ + if (iTeam == view_as(TFTeam_Red)) return view_as(TFTeam_Blue); + + return view_as(TFTeam_Red); +} + +/** + * Selects a target for a homing rocket based on team and optional directional weighting. + * Falls back to random selection if rocket entity is invalid. + */ +stock int SelectTarget(int iTeam, int iRocket = -1) +{ + int iTarget = -1; + float fTargetWeight = 0.0; + float fRocketPosition[3]; + float fRocketDirection[3]; + float fWeight; + bool bUseRocket = false; + int iOwner = -1; + + if (iRocket != -1) + { + int iEntity = EntRefToEntIndex(RocketEntity[iRocket]); + + // Validate entity before any property access to prevent crashes + if (iEntity != -1 && IsValidEntity(iEntity)) + { + int iClass = RocketClass[iRocket]; + iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + + GetEntPropVector(iEntity, Prop_Send, "m_vecOrigin", fRocketPosition); + CopyVectors(RocketDirection[iRocket], fRocketDirection); + fWeight = RocketClassTargetWeight[iClass]; + + bUseRocket = true; + } + // If entity is invalid, bUseRocket stays false and we fall back to random selection + } + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsValidClientEx(iClient, true)) continue; + if (iTeam && GetClientTeam(iClient) != iTeam) continue; + if (iClient == iOwner) continue; + + float fNewWeight = GetURandomFloatRange(0.0, 100.0); + + if (bUseRocket) + { + float fClientPosition[3]; GetClientEyePosition(iClient, fClientPosition); + float fDirectionToClient[3]; MakeVectorFromPoints(fRocketPosition, fClientPosition, fDirectionToClient); + + fNewWeight += GetVectorDotProduct(fRocketDirection, fDirectionToClient) * fWeight; + } + + if ((iTarget == -1) || fNewWeight >= fTargetWeight) + { + iTarget = iClient; + fTargetWeight = fNewWeight; + } + } + + return iTarget; +} + +// ********************************************************************************* +// EFFECT & RESOURCE UTILITIES +// ********************************************************************************* + +/** + * Shows a hidden MOTD panel, often used for streaming audio. + */ +stock void ShowHiddenMOTDPanel(int iClient, char[] strTitle, char[] strMsg, char[] strType = "2") +{ + KeyValues hPanel = new KeyValues("data"); + hPanel.SetString("title", strTitle); + hPanel.SetString("type", strType); + hPanel.SetString("msg", strMsg); + ShowVGUIPanel(iClient, "info", hPanel, false); + delete hPanel; +} + +/** + * Plays a particle system. + */ +stock void PlayParticle(float fPosition[3], float fAngles[3], char[] strParticleName, float fEffectTime = 5.0, float fLifeTime = 9.0) +{ + int iEntity = CreateEntityByName("info_particle_system"); + + if (iEntity == -1) + { + LogError("PlayParticle: could not create info_particle_system"); + return; + } + + TeleportEntity(iEntity, fPosition, fAngles, NULL_VECTOR); + DispatchKeyValue(iEntity, "effect_name", strParticleName); + ActivateEntity(iEntity); + AcceptEntityInput(iEntity, "Start"); + CreateTimer(fEffectTime, StopParticle, EntIndexToEntRef(iEntity)); + CreateTimer(fLifeTime, KillParticle, EntIndexToEntRef(iEntity)); +} + +/** + * Timer callback to stop a particle system. + */ +public Action StopParticle(Handle hTimer, any iEntityRef) +{ + if (iEntityRef == INVALID_ENT_REFERENCE) return Plugin_Continue; + + int iEntity = EntRefToEntIndex(iEntityRef); + if (iEntity != INVALID_ENT_REFERENCE) + { + AcceptEntityInput(iEntity, "Stop"); + } + + return Plugin_Continue; +} + +/** + * Timer callback to kill a particle system entity. + */ +public Action KillParticle(Handle hTimer, any iEntityRef) +{ + if (iEntityRef == INVALID_ENT_REFERENCE) return Plugin_Continue; + + int iEntity = EntRefToEntIndex(iEntityRef); + if (iEntity != INVALID_ENT_REFERENCE) + { + RemoveEntity(iEntity); + } + + return Plugin_Continue; +} + +/** + * Precaches a particle system by creating and quickly destroying it. + */ +stock void PrecacheParticle(char[] strParticleName) +{ + PlayParticle(view_as({0.0, 0.0, 0.0}), view_as({0.0, 0.0, 0.0}), strParticleName, 0.1, 0.1); +} + +/** + * Precaches a sound and adds it to the download table. + */ +stock void PrecacheSoundEx(char[] strFileName, bool bPreload = false, bool bAddToDownloadTable = false) +{ + char strFinalPath[PLATFORM_MAX_PATH]; + FormatEx(strFinalPath, sizeof(strFinalPath), "sound/%s", strFileName); + PrecacheSound(strFileName, bPreload); + + if (bAddToDownloadTable) AddFileToDownloadsTable(strFinalPath); +} + +/** + * Removes illegal characters from a string. + */ +stock void CleanString(char[] strBuffer) +{ + int iLength = strlen(strBuffer); + for (int iPos = 0; iPos < iLength; iPos++) + { + switch (strBuffer[iPos]) + { + case '\r', '\n', '\t': strBuffer[iPos] = ' '; + } + } + TrimString(strBuffer); +} + +/** + * Precaches a model and its dependencies and adds them to the download table. + */ +stock void PrecacheModelEx(char[] strFileName, bool bPreload = false, bool bAddToDownloadTable = false) +{ + PrecacheModel(strFileName, bPreload); + + if (!bAddToDownloadTable) return; + + char strDepFileName[PLATFORM_MAX_PATH]; + FormatEx(strDepFileName, sizeof(strDepFileName), "%s.res", strFileName); + + if (!FileExists(strDepFileName)) return; + + File hStream = OpenFile(strDepFileName, "r"); + if (hStream == null) + { + LogError("Error, can't read file containing model dependencies: %s", strDepFileName); + return; + } + + while(!hStream.EndOfFile()) + { + char strBuffer[PLATFORM_MAX_PATH]; + hStream.ReadLine(strBuffer, sizeof(strBuffer)); + CleanString(strBuffer); + + if (!FileExists(strBuffer, true)) continue; + + if (StrContains(strBuffer, ".vmt", false) != -1) PrecacheDecal(strBuffer, true); + else if (StrContains(strBuffer, ".mdl", false) != -1) PrecacheModel(strBuffer, true); + else if (StrContains(strBuffer, ".pcf", false) != -1) PrecacheGeneric(strBuffer, true); + + AddFileToDownloadsTable(strBuffer); + } + + delete hStream; +} + +/** + * Custom trace ray filter to exclude a specific entity. + */ +public bool TEF_ExcludeEntity(int iEntity, int iContentsMask, any aData) +{ + return (iEntity != aData); +} \ No newline at end of file diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/morecolors.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/morecolors.inc index 1662b7f0..b1a62854 100755 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/morecolors.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/morecolors.inc @@ -7,9 +7,7 @@ #endif #define _more_colors_included -#pragma semicolon 1 -#pragma newdecls required - +#pragma newdecls optional #include #define MORE_COLORS_VERSION "2.0.0-MC" @@ -23,15 +21,11 @@ #define MC_GAME_DODS 0 -// hack for no sm 1.12 -// -sappho -#define TF_MAXPLAYERS 101 - -bool MC_SkipList[TF_MAXPLAYERS + 1]; +bool MC_SkipList[MAXPLAYERS+1]; StringMap MC_Trie; int MC_TeamColors[][] = {{0xCCCCCC, 0x4D7942, 0xFF4040}}; // Multi-dimensional array for games that don't support SayText2. First index is the game index (as defined by the GAME_ defines), second index is team. 0 = spectator, 1 = team1, 2 = team2 -static ConVar sm_show_activity = null; +static ConVar sm_show_activity; /** * Prints a message to a specific client in the chat area. @@ -112,11 +106,11 @@ stock void MC_PrintToChatEx(int client, int author, const char[] message, any .. ThrowError("Client %i is not in game", client); } - if (author <= 0 || author > MaxClients) { + if (author < 0 || author > MaxClients) { ThrowError("Invalid client index %i", author); } - if (!IsClientInGame(author)) { + if ((author != 0) && !IsClientInGame(author)) { ThrowError("Client %i is not in game", author); } @@ -140,11 +134,11 @@ stock void MC_PrintToChatEx(int client, int author, const char[] message, any .. stock void MC_PrintToChatAllEx(int author, const char[] message, any ...) { MC_CheckTrie(); - if (author <= 0 || author > MaxClients) { + if (author < 0 || author > MaxClients) { ThrowError("Invalid client index %i", author); } - if (!IsClientInGame(author)) { + if ((author != 0) && !IsClientInGame(author)) { ThrowError("Client %i is not in game", author); } @@ -265,6 +259,7 @@ stock void MC_ReplaceColorCodes(char[] buffer, int author = 0, bool removeTags = ReplaceString(buffer, maxlen, "{default}", "", false); ReplaceString(buffer, maxlen, "{teamcolor}", "", false); } + if (author != 0 && !removeTags) { if (author < 0 || author > MaxClients) { ThrowError("Invalid client index %i", author); @@ -286,12 +281,13 @@ stock void MC_ReplaceColorCodes(char[] buffer, int author = 0, bool removeTags = // Since the string's size is going to be changing, output will hold the replaced string and we'll search buffer Regex regex = new Regex("{[#a-zA-Z0-9]+}"); - for(int i = 0; i < 1000; i++) { // The RegEx extension is quite flaky, so we have to loop here :/. This loop is supposed to be infinite and broken by return, but conditions have been added to be safe. + for (int i = 0; i < 1000; i++) { // The RegEx extension is quite flaky, so we have to loop here :/. This loop is supposed to be infinite and broken by return, but conditions have been added to be safe. if (regex.Match(buffer[cursor]) < 1) { delete regex; strcopy(buffer, maxlen, output); return; } + regex.GetSubString(0, tag, sizeof(tag)); MC_StrToLower(tag); cursor = StrContains(buffer[cursor], tag, false) + cursor + 1; @@ -343,11 +339,12 @@ stock void MC_ReplaceColorCodes(char[] buffer, int author = 0, bool removeTags = */ stock void CSubString(const char[] input, char[] output, int maxlen, int start, int numChars = 0) { int i = 0; - for(;;) { + for (;;) { if (i == maxlen - 1 || i >= numChars || input[start + i] == '\0') { output[i] = '\0'; return; } + output[i] = input[start + i]; i++; } @@ -360,7 +357,7 @@ stock void CSubString(const char[] input, char[] output, int maxlen, int start, */ stock void MC_StrToLower(char[] buffer) { int len = strlen(buffer); - for(int i = 0; i < len; i++) { + for (int i = 0; i < len; i++) { buffer[i] = CharToLower(buffer[i]); } } @@ -414,8 +411,7 @@ stock void MC_ReplyToCommand(int client, const char[] message, any ...) { MC_RemoveTags(buffer, sizeof(buffer)); PrintToServer("%s", buffer); } - - if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { MC_RemoveTags(buffer, sizeof(buffer)); PrintToConsole(client, "%s", buffer); } @@ -440,8 +436,7 @@ stock void MC_ReplyToCommandEx(int client, int author, const char[] message, any MC_RemoveTags(buffer, sizeof(buffer)); PrintToServer("%s", buffer); } - - if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { MC_RemoveTags(buffer, sizeof(buffer)); PrintToConsole(client, "%s", buffer); } @@ -486,20 +481,17 @@ stock int MC_ShowActivity(int client, const char[] format, any ...) { GetClientName(client, name, sizeof(name)); AdminId id = GetUserAdmin(client); - if (id == INVALID_ADMIN_ID - || !id.HasFlag(Admin_Generic, Access_Effective)) - { + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { sign = "PLAYER"; } /* Display the message to the client? */ - if (replyto == SM_REPLY_TO_CONSOLE) - { + if (replyto == SM_REPLY_TO_CONSOLE) { SetGlobalTransTarget(client); VFormat(szBuffer, sizeof(szBuffer), format, 3); MC_RemoveTags(szBuffer, sizeof(szBuffer)); - PrintToConsole(client, "%s%s\n", tag, szBuffer); + PrintToConsole(client, "%s%s", tag, szBuffer); display_in_chat = true; } } @@ -508,7 +500,7 @@ stock int MC_ShowActivity(int client, const char[] format, any ...) { VFormat(szBuffer, sizeof(szBuffer), format, 3); MC_RemoveTags(szBuffer, sizeof(szBuffer)); - PrintToServer("%s%s\n", tag, szBuffer); + PrintToServer("%s%s", tag, szBuffer); } if (!value) { @@ -516,24 +508,23 @@ stock int MC_ShowActivity(int client, const char[] format, any ...) { } for (int i = 1; i <= MaxClients; ++i) { - if (!IsClientInGame(i) - || IsFakeClient(i) - || (display_in_chat && i == client)) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { continue; } AdminId id = GetUserAdmin(i); SetGlobalTransTarget(i); - if (id == INVALID_ADMIN_ID - || !id.HasFlag(Admin_Generic, Access_Effective)) { + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { /* Treat this as a normal user. */ if ((value & 1) | (value & 2)) { char newsign[MAX_NAME_LENGTH]; - newsign = sign; if ((value & 2) || (i == client)) { newsign = name; } + else { + newsign = sign; + } VFormat(szBuffer, sizeof(szBuffer), format, 3); @@ -543,15 +534,15 @@ stock int MC_ShowActivity(int client, const char[] format, any ...) { else { /* Treat this as an admin user */ bool is_root = id.HasFlag(Admin_Root, Access_Effective); - if ((value & 4) - || (value & 8) - || ((value & 16) && is_root)) { + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { char newsign[MAX_NAME_LENGTH]; - newsign = sign; if ((value & 8) || ((value & 16) && is_root) || (i == client)) { newsign = name; } + else { + newsign = sign; + } VFormat(szBuffer, sizeof(szBuffer), format, 3); @@ -574,8 +565,13 @@ stock int MC_ShowActivity(int client, const char[] format, any ...) { * @error */ stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, any ...) { - if (sm_show_activity == null) + if (sm_show_activity == null) { sm_show_activity = FindConVar("sm_show_activity"); + } + + char szTag[MC_MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); char szBuffer[MC_MAX_MESSAGE_LENGTH]; //char szCMessage[MC_MAX_MESSAGE_LENGTH]; @@ -592,8 +588,7 @@ stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, a GetClientName(client, name, sizeof(name)); AdminId id = GetUserAdmin(client); - if (id == INVALID_ADMIN_ID - || !id.HasFlag(Admin_Generic, Access_Effective)) { + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { sign = "PLAYER"; } @@ -603,7 +598,7 @@ stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, a VFormat(szBuffer, sizeof(szBuffer), format, 4); MC_RemoveTags(szBuffer, sizeof(szBuffer)); - PrintToConsole(client, "%s%s\n", tag, szBuffer); + PrintToConsole(client, "%s%s", szTag, szBuffer); display_in_chat = true; } } @@ -612,7 +607,7 @@ stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, a VFormat(szBuffer, sizeof(szBuffer), format, 4); MC_RemoveTags(szBuffer, sizeof(szBuffer)); - PrintToServer("%s%s\n", tag, szBuffer); + PrintToServer("%s%s", szTag, szBuffer); } if (!value) { @@ -620,24 +615,23 @@ stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, a } for (int i = 1; i <= MaxClients; ++i) { - if (!IsClientInGame(i) - || IsFakeClient(i) - || (display_in_chat && i == client)) { + if (!IsClientInGame(i) || IsFakeClient(i) || (display_in_chat && i == client)) { continue; } AdminId id = GetUserAdmin(i); SetGlobalTransTarget(i); - if (id == INVALID_ADMIN_ID - || !id.HasFlag(Admin_Generic, Access_Effective)) { + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { /* Treat this as a normal user. */ if ((value & 1) | (value & 2)) { char newsign[MAX_NAME_LENGTH]; - newsign = sign; if ((value & 2) || (i == client)) { newsign = name; } + else { + newsign = sign; + } VFormat(szBuffer, sizeof(szBuffer), format, 4); @@ -647,15 +641,15 @@ stock int MC_ShowActivityEx(int client, const char[] tag, const char[] format, a else { /* Treat this as an admin user */ bool is_root = id.HasFlag(Admin_Root, Access_Effective); - if ((value & 4) - || (value & 8) - || ((value & 16) && is_root)) { + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { char newsign[MAX_NAME_LENGTH]; - newsign = sign; if ((value & 8) || ((value & 16) && is_root) || (i == client)) { newsign = name; } + else { + newsign = sign; + } VFormat(szBuffer, sizeof(szBuffer), format, 4); @@ -684,6 +678,10 @@ stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, an sm_show_activity = FindConVar("sm_show_activity"); } + char szTag[MC_MAX_MESSAGE_LENGTH]; + strcopy(szTag, sizeof(szTag), tag); + MC_RemoveTags(szTag, sizeof(szTag)); + char szBuffer[MC_MAX_MESSAGE_LENGTH]; //char szCMessage[MC_MAX_MESSAGE_LENGTH]; int value = sm_show_activity.IntValue; @@ -700,8 +698,7 @@ stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, an GetClientName(client, name, sizeof(name)); AdminId id = GetUserAdmin(client); - if (id == INVALID_ADMIN_ID - || !id.HasFlag(Admin_Generic, Access_Effective)) { + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { sign = "PLAYER"; } @@ -712,14 +709,14 @@ stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, an * simply gets added to the console, so we don't want it to print * twice. */ - MC_PrintToChatEx(client, client, "%s%s", tag, szBuffer); + MC_PrintToChatEx(client, client, "%s%s", szTag, szBuffer); } else { SetGlobalTransTarget(LANG_SERVER); VFormat(szBuffer, sizeof(szBuffer), format, 4); MC_RemoveTags(szBuffer, sizeof(szBuffer)); - PrintToServer("%s%s\n", tag, szBuffer); + PrintToServer("%s%s", szTag, szBuffer); } if (!value) { @@ -727,24 +724,23 @@ stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, an } for (int i = 1; i <= MaxClients; ++i) { - if (!IsClientInGame(i) - || IsFakeClient(i) - || i == client) { + if (!IsClientInGame(i) || IsFakeClient(i) || i == client) { continue; } AdminId id = GetUserAdmin(i); SetGlobalTransTarget(i); - if (id == INVALID_ADMIN_ID - || !id.HasFlag(Admin_Generic, Access_Effective)) { + if (id == INVALID_ADMIN_ID || !id.HasFlag(Admin_Generic, Access_Effective)) { /* Treat this as a normal user. */ if ((value & 1) | (value & 2)) { char newsign[MAX_NAME_LENGTH]; - newsign = sign; if ((value & 2)) { newsign = name; } + else { + newsign = sign; + } VFormat(szBuffer, sizeof(szBuffer), format, 4); @@ -754,15 +750,16 @@ stock int MC_ShowActivity2(int client, const char[] tag, const char[] format, an else { /* Treat this as an admin user */ bool is_root = id.HasFlag(Admin_Root, Access_Effective); - if ((value & 4) - || (value & 8) - || ((value & 16) && is_root)) { + if ((value & 4) || (value & 8) || ((value & 16) && is_root)) { char newsign[MAX_NAME_LENGTH]; - newsign = sign; + if ((value & 8) || ((value & 16) && is_root)) { newsign = name; } + else { + newsign = sign; + } VFormat(szBuffer, sizeof(szBuffer), format, 4); @@ -996,6 +993,6 @@ stock StringMap MC_InitColorTrie() { hTrie.SetValue("whitesmoke", 0xF5F5F5); hTrie.SetValue("yellow", 0xFFFF00); hTrie.SetValue("yellowgreen", 0x9ACD32); - + return hTrie; } diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/multicolors.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/multicolors.inc new file mode 100644 index 00000000..ba1976bd --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/multicolors.inc @@ -0,0 +1,461 @@ +#if defined _multicolors_included + #endinput +#endif +#define _multicolors_included + +#define MuCo_VERSION "2.1.2" + +#include "morecolors" +#include "colors" + +/* +* +* Credits: +* - Popoklopsi +* - Powerlord +* - exvel +* - Dr. McKay +* +* Based on stamm-colors +* - https://github.com/popoklopsi/Stamm/blob/master/include/stamm/stamm-colors.inc +* +*/ + + + +#define PREFIX_MAX_LENGTH 64 +#define PREFIX_SEPARATOR "{default} " + +/* Global var to check whether colors are fixed or not */ +static bool g_bCFixColors; + +static char g_sCPrefix[PREFIX_MAX_LENGTH]; + +/** + * Add a chat prefix before all chat msg + * + * @param sPrefix Prefix + */ +stock void CSetPrefix(const char[] sPrefix, any ...) { + if (!sPrefix[0]) { + return; + } + + SetGlobalTransTarget(LANG_SERVER); + VFormat(g_sCPrefix, sizeof(g_sCPrefix) - strlen(PREFIX_SEPARATOR), sPrefix, 2); + + // Add ending space + Format(g_sCPrefix, sizeof(g_sCPrefix), "%s%s", g_sCPrefix, PREFIX_SEPARATOR); +} + +/** + * Add a chat prefix before all chat msg + * + * @param sPrefix Prefix + */ +stock void CClearPrefix() { + g_sCPrefix[0] = '\0'; +} + +/** + * Writes a message to a client with the correct stock for the game. + * + * @param client Client index. + * @param message Message (formatting rules). + * + * @error If the client is not connected an error will be thrown. + */ +stock void CPrintToChat(int client, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_PrintToChat(client, "%s%s", g_sCPrefix, buffer); + } + else { + MC_PrintToChat(client, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules) + */ +stock void CPrintToChatAll(const char[] message, any ...) { + if (!g_bCFixColors) { + CFixColors(); + } + + char buffer[MAX_BUFFER_LENGTH]; + if (!IsSource2009()) { + for (int i = 1; i <= MaxClients; ++i) { + if (IsClientInGame(i) && !IsFakeClient(i) && !C_SkipList[i]) { + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), message, 2); + + C_PrintToChat(i, "%s", buffer); + } + + C_SkipList[i] = false; + } + } + else { + MC_CheckTrie(); + + char buffer2[MAX_BUFFER_LENGTH]; + + for (int i = 1; i <= MaxClients; ++i) { + if (!IsClientInGame(i) || MC_SkipList[i]) { + MC_SkipList[i] = false; + continue; + } + + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 2); + + MC_ReplaceColorCodes(buffer2); + MC_SendMessage(i, buffer2); + } + } +} + +/** + * Writes a message to all of a client's observers. + * + * @param target Client index. + * @param message Message (formatting rules). + */ +stock void CPrintToChatObservers(int target, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + for (int client = 1; client <= MaxClients; client++) { + if (IsClientInGame(client) && !IsPlayerAlive(client) && !IsFakeClient(client)) { + int observee = GetEntPropEnt(client, Prop_Send, "m_hObserverTarget"); + int ObserverMode = GetEntProp(client, Prop_Send, "m_iObserverMode"); + + if (observee == target && (ObserverMode == 4 || ObserverMode == 5)) { + CPrintToChat(client, buffer); + } + } + } +} + +/** + * Writes a message to a client with the correct stock for the game. + * + * @param client Client index. + * @param author Author index. + * @param message Message (formatting rules). + * + * @error If the client is not connected an error will be thrown. + */ +stock void CPrintToChatEx(int client, int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_PrintToChatEx(client, author, "%s%s", g_sCPrefix, buffer); + } + else { + MC_PrintToChatEx(client, author, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Writes a message to all clients with the correct stock for the game. + * + * @param author Author index. + * @param message Message (formatting rules). + */ +stock void CPrintToChatAllEx(int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_PrintToChatAllEx(author, "%s%s", g_sCPrefix, buffer); + } + else { + MC_PrintToChatAllEx(author, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Writes a message to all of a client's observers with the correct + * game stock. + * + * @param target Client index. + * @param message Message (formatting rules). + */ +stock void CPrintToChatObserversEx(int target, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + for (int client = 1; client <= MaxClients; client++) { + if (IsClientInGame(client) && !IsPlayerAlive(client) && !IsFakeClient(client)) { + int observee = GetEntPropEnt(client, Prop_Send, "m_hObserverTarget"); + int ObserverMode = GetEntProp(client, Prop_Send, "m_iObserverMode"); + + if (observee == target && (ObserverMode == 4 || ObserverMode == 5)) { + CPrintToChatEx(client, target, buffer); + } + } + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param message Message (formatting rules) + */ +stock void CReplyToCommand(int client, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ReplyToCommand(client, "%s%s", g_sCPrefix, buffer); + } + else { + MC_ReplyToCommand(client, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param author Client to use for {teamcolor} + * @param message Message (formatting rules) + */ +stock void CReplyToCommandEx(int client, int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ReplyToCommandEx(client, author, "%s%s", g_sCPrefix, buffer); + } + else { + MC_ReplyToCommandEx(client, author, "%s%s", g_sCPrefix, buffer); + } +} + +/** + * Remove all tags and print to server + * + * @param message Message (formatting rules) + */ +stock void CPrintToServer(const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + char prefixBuffer[PREFIX_MAX_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 2); + strcopy(prefixBuffer, sizeof(prefixBuffer), g_sCPrefix); + + if (!g_bCFixColors) { + CFixColors(); + } + + CRemoveTags(buffer, sizeof(buffer)); + CRemoveTags(prefixBuffer, sizeof(prefixBuffer)); + + PrintToServer("%s%s", prefixBuffer, buffer); +} + +/** + * Displays usage of an admin command to users depending on the + * setting of the sm_show_activity cvar. + * + * This version does not display a message to the originating client + * if used from chat triggers or menus. If manual replies are used + * for these cases, then this function will suffice. Otherwise, + * CShowActivity2() is slightly more useful. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * + * @error + */ +stock void CShowActivity(int author, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 3); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ShowActivity(author, "%s", buffer); + } + else { + MC_ShowActivity(author, "%s", buffer); + } +} + +/** + * Same as C_ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing). + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to display with. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * + * @error + */ +stock void CShowActivityEx(int author, const char[] tag, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ShowActivityEx(author, tag, "%s", buffer); + } + else { + MC_ShowActivityEx(author, tag, "%s", buffer); + } +} + +/** + * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar. + * All users receive a message in their chat text, except for the originating client, + * who receives the message based on the current ReplySource. + * Supports color tags. + * + * @param client Client index doing the action, or 0 for server. + * @param tags Tag to prepend to the message. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * + * @error + */ + stock void CShowActivity2(int author, const char[] tag, const char[] message, any ...) { + char buffer[MAX_MESSAGE_LENGTH]; + SetGlobalTransTarget(LANG_SERVER); + VFormat(buffer, sizeof(buffer), message, 4); + + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + C_ShowActivity2(author, tag, "%s", buffer); + } + else { + MC_ShowActivity2(author, tag, "%s", buffer); + } +} + + +/** + * Replaces color tags in a string with color codes + * + * @param message String. + * @param maxlength Maximum length of the string buffer. + */ +stock void CFormatColor(char[] message, int maxlength, int author = -1) { + if (!g_bCFixColors) { + CFixColors(); + } + + if (!IsSource2009()) { + if (author == 0) { + author = -1; + } + + C_Format(message, maxlength, author); + } + else { + if (author == -1) { + author = 0; + } + + MC_ReplaceColorCodes(message, author, false, maxlength); + } +} + +/** + * Removes color tags from a message + * + * @param message Message to remove tags from + * @param maxlen Maximum buffer length + */ +stock void CRemoveTags(char[] message, int maxlen) { + if (!IsSource2009()) { + C_RemoveTags(message, maxlen); + } + else { + MC_RemoveTags(message, maxlen); + } +} + +/** + * Fixes missing Lightgreen color. + */ +stock void CFixColors() { + g_bCFixColors = true; + + // Replace lightgreen if not exists + if (!C_ColorAllowed(Color_Lightgreen)) { + if (C_ColorAllowed(Color_Lime)) { + C_ReplaceColor(Color_Lightgreen, Color_Lime); + } + else if (C_ColorAllowed(Color_Olive)) { + C_ReplaceColor(Color_Lightgreen, Color_Olive); + } + } +} + +stock bool IsSource2009() { + return (GetEngineVersion() == Engine_CSS + || GetEngineVersion() == Engine_HL2DM + || GetEngineVersion() == Engine_DODS + || GetEngineVersion() == Engine_TF2 + || GetEngineVersion() == Engine_SDK2013); +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc new file mode 100644 index 00000000..5e56ce1e --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc @@ -0,0 +1,994 @@ +#if defined _tfdb_included + #endinput +#endif +#define _tfdb_included + +// ---- General settings ----------------------------------------------------------- +// +// Timer rate vs OnGameFrame: +// SourceMod's TimerSys.cpp defines TIMER_MIN_ACCURACY = 0.1 (seconds). +// The timer processing loop (RunFrame) only fires every ~0.1s regardless of +// the interval passed to CreateTimer — so the logic timer runs at ~10Hz. +// The actual fire interval is NOT exactly 0.1s: it depends on tickrate since +// 0.1s must be rounded up to the next tick boundary (e.g. 7 ticks on 66-tick +// = 0.1061s, 13 ticks on 128-tick = 0.1016s). +// +// OnGameFrame fires every server tick (~66Hz on 66-tick, ~128Hz on 128-tick). +// We use it for smooth per-frame homing. Turn rate and elevation values in the +// config are calibrated as "amount per 0.1s" for backwards compatibility. +// Per-frame scaling uses GetTickInterval() directly — no derived magic numbers. +// +#define FPS_LOGIC_RATE 10.0 +#define FPS_LOGIC_INTERVAL 1.0 / FPS_LOGIC_RATE + +/** + * The base time interval (seconds) that config turn rate / elevation values + * are calibrated against. Turn rate 0.05 means "lerp 0.05 per 0.1s." + * Per-frame code divides by this and multiplies by GetTickInterval(). + */ +#define TICK_BASE_INTERVAL 0.1 + +/** Conversion factor: Hammer units/s to MPH. (1 HU ≈ 0.01905m) */ +#define HAMMER_TO_MPH 0.042614 + +// ---- Maximum structure sizes ---------------------------------------------------- +#define MAX_ROCKETS 100 +#define MAX_ROCKET_CLASSES 50 +#define MAX_SPAWNER_CLASSES 50 +#define MAX_SPAWN_POINTS 100 +#define MAX_PRESETS 16 + +// ---- Flags and types constants -------------------------------------------------- + +/** + * Rocket behaviour types that determine how rockets track targets. + */ +enum BehaviourTypes +{ + Behaviour_Unknown, /**< Unknown/undefined behaviour */ + Behaviour_Homing, /**< Modern homing - smooth tracking with turn rate */ + Behaviour_LegacyHoming /**< Legacy homing - instant direction changes */ +}; + +/** + * Rocket behaviour flags (bitfield). Combine using bitwise OR (|). + * Use TestFlags() macro to check if a flag is set. + */ +enum RocketFlags +{ + RocketFlag_None = 0, /**< No flags set */ + RocketFlag_PlaySpawnSound = 1 << 0, /**< Play sound when rocket spawns */ + RocketFlag_PlayBeepSound = 1 << 1, /**< Play periodic beep sound */ + RocketFlag_PlayAlertSound = 1 << 2, /**< Play alert sound when target changes */ + RocketFlag_ElevateOnDeflect = 1 << 3, /**< Rocket elevates after deflection */ + RocketFlag_IsNeutral = 1 << 4, /**< Rocket has no team (grey skin) */ + RocketFlag_Exploded = 1 << 5, /**< Rocket has exploded (internal) */ + RocketFlag_OnSpawnCmd = 1 << 6, /**< Execute commands on spawn */ + RocketFlag_OnDeflectCmd = 1 << 7, /**< Execute commands on deflect */ + RocketFlag_OnKillCmd = 1 << 8, /**< Execute commands on kill */ + RocketFlag_OnExplodeCmd = 1 << 9, /**< Execute commands on explode */ + RocketFlag_CustomModel = 1 << 10, /**< Uses custom model */ + RocketFlag_CustomSpawnSound = 1 << 11, /**< Uses custom spawn sound */ + RocketFlag_CustomBeepSound = 1 << 12, /**< Uses custom beep sound */ + RocketFlag_CustomAlertSound = 1 << 13, /**< Uses custom alert sound */ + RocketFlag_Elevating = 1 << 14, /**< Currently elevating (internal) */ + RocketFlag_IsAnimated = 1 << 15, /**< Model has animations */ + RocketFlag_IsTRLimited = 1 << 16, /**< Turn rate has a limit */ + RocketFlag_IsSpeedLimited = 1 << 17, /**< Speed has a limit */ + RocketFlag_KeepDirection = 1 << 18, /**< Maintain direction after deflect */ + RocketFlag_TeamlessHits = 1 << 19, /**< Can hit players of any team */ + RocketFlag_ResetBounces = 1 << 20, /**< Reset bounce count on deflect */ + RocketFlag_NoBounceDrags = 1 << 21, /**< Can't drag while bouncing */ + RocketFlag_OnNoTargetCmd = 1 << 22, /**< Execute commands when no target */ + RocketFlag_CanBeStolen = 1 << 23, /**< Rocket can be stolen by other players */ + RocketFlag_StealTeamCheck = 1 << 24 /**< Only teammates can steal */ +}; + +/** + * Sound types for rocket audio. + */ +enum RocketSound +{ + RocketSound_Spawn, /**< Sound played when rocket spawns */ + RocketSound_Beep, /**< Periodic tracking beep sound */ + RocketSound_Alert /**< Sound played when target changes */ +}; + +/** + * Rocket state flags (bitfield). Indicates current rocket status. + */ +enum RocketState +{ + RocketState_None = 0, /**< Default state */ + RocketState_Dragging = 1 << 0, /**< Player is dragging the rocket */ + RocketState_Bouncing = 1 << 1, /**< Rocket is bouncing off surfaces */ + RocketState_Stolen = 1 << 2, /**< Rocket was stolen from another player */ + RocketState_Delayed = 1 << 3, /**< Rocket is in delay prevention mode */ + RocketState_CanDrag = 1 << 4 /**< Rocket can be dragged by owner */ +}; + +/** + * Steal information for a client. + */ +enum struct eRocketSteal +{ + bool stoleRocket; /**< Whether client stole a rocket this round */ + int rocketsStolen; /**< Number of rockets stolen this round */ +} + +/** Macro to test if flags are set: TestFlags(flags, RocketFlag_PlaySpawnSound) */ +#define TestFlags(%1,%2) (!!((%1) & (%2))) + +// ---- Other resources ------------------------------------------------------------ +#define SOUND_DEFAULT_SPAWN "weapons/sentry_rocket.wav" +#define SOUND_DEFAULT_BEEP "weapons/sentry_scan.wav" +#define SOUND_DEFAULT_ALERT "weapons/sentry_spot.wav" +#define SOUND_DEFAULT_SPEEDUP "misc/doomsday_lift_warning.wav" +#define SNDCHAN_MUSIC 32 +#define PARTICLE_NUKE_1 "fireSmokeExplosion" +#define PARTICLE_NUKE_2 "fireSmokeExplosion1" +#define PARTICLE_NUKE_3 "fireSmokeExplosion2" +#define PARTICLE_NUKE_4 "fireSmokeExplosion3" +#define PARTICLE_NUKE_5 "fireSmokeExplosion4" +#define PARTICLE_NUKE_COLLUMN "fireSmoke_collumnP" +#define PARTICLE_NUKE_1_ANGLES view_as({270.0, 0.0, 0.0}) +#define PARTICLE_NUKE_2_ANGLES PARTICLE_NUKE_1_ANGLES +#define PARTICLE_NUKE_3_ANGLES PARTICLE_NUKE_1_ANGLES +#define PARTICLE_NUKE_4_ANGLES PARTICLE_NUKE_1_ANGLES +#define PARTICLE_NUKE_5_ANGLES PARTICLE_NUKE_1_ANGLES +#define PARTICLE_NUKE_COLLUMN_ANGLES PARTICLE_NUKE_1_ANGLES + +// ---- Forwards -------------------------------------------------- + +/** + * Called when a rocket has been created and spawned. + * + * @param iIndex The rocket's internal index (0 to MAX_ROCKETS-1). + * @param iEntity The rocket's entity index. + */ +forward void TFDB_OnRocketCreated(int iIndex, int iEntity); + +/** + * Called before a rocket is created. Return Plugin_Handled to prevent creation. + * + * @param iIndex The rocket's internal index (0 to MAX_ROCKETS-1). + * @param iClass The rocket class index. Can be modified. + * @param iFlags The rocket flags. Can be modified. + * @return Plugin_Continue to allow, Plugin_Handled to block. + */ +forward Action TFDB_OnRocketCreatedPre(int iIndex, int &iClass, RocketFlags &iFlags); + +/** + * Called when a rocket has been deflected by a player. + * + * @param iIndex The rocket's internal index. + * @param iEntity The rocket's entity index. + * @param iOwner The client index of the new owner (deflector). + */ +forward void TFDB_OnRocketDeflect(int iIndex, int iEntity, int iOwner); + +/** + * Called before a rocket deflection. Return Plugin_Handled to prevent deflection. + * + * @param iIndex The rocket's internal index. + * @param iEntity The rocket's entity index. + * @param iOwner The client index of the new owner (deflector). + * @param iTarget The new target client. Can be modified. + * @return Plugin_Continue to allow, Plugin_Handled to block. + */ +forward Action TFDB_OnRocketDeflectPre(int iIndex, int iEntity, int iOwner, int &iTarget); + +/** + * Called when a player steals another player's rocket. + * + * @param iIndex The rocket's internal index. + * @param iOwner The client who stole the rocket. + * @param iTarget The original target who lost the rocket. + * @param iStealCount The number of times this player has stolen rockets this round. + */ +forward void TFDB_OnRocketSteal(int iIndex, int iOwner, int iTarget, int iStealCount); + +/** + * Called when a rocket has no valid target available. + * + * @param iIndex The rocket's internal index. + * @param iTarget The previous target (may be invalid). + * @param iOwner The rocket's current owner. + */ +forward void TFDB_OnRocketNoTarget(int iIndex, int iTarget, int iOwner); + +/** + * Called when a rocket enters delay prevention mode (player is delaying). + * + * @param iIndex The rocket's internal index. + * @param iTarget The target player who is delaying. + */ +forward void TFDB_OnRocketDelay(int iIndex, int iTarget); + +/** + * Called when a rocket bounces off a surface. + * + * @param iIndex The rocket's internal index. + * @param iEntity The rocket's entity index. + */ +forward void TFDB_OnRocketBounce(int iIndex, int iEntity); + +/** + * Called before a rocket bounces. Return Plugin_Handled to prevent bounce. + * + * @param iIndex The rocket's internal index. + * @param iEntity The rocket's entity index. + * @param fAngles The bounce angles. Can be modified. + * @param fVelocity The bounce velocity. Can be modified. + * @return Plugin_Continue to allow, Plugin_Handled to block. + */ +forward Action TFDB_OnRocketBouncePre(int iIndex, int iEntity, float fAngles[3], float fVelocity[3]); + +/** + * Called after a rockets configuration file has been loaded and parsed. + * + * @param strConfigFile The name of the configuration file that was loaded. + */ +forward void TFDB_OnRocketsConfigExecuted(const char[] strConfigFile); + +/** + * Called when a rocket's state changes. + * + * @param iIndex The rocket's internal index. + * @param iState The previous state. + * @param iNewState The new state. + */ +forward void TFDB_OnRocketStateChanged(int iIndex, RocketState iState, RocketState iNewState); + +// ---- Natives -------------------------------------------------- + +// ============ Rocket Instance Functions ============ + +/** + * Checks if a rocket index is valid and the rocket exists. + * + * @param iIndex The rocket's internal index (0 to MAX_ROCKETS-1). + * @return True if the rocket exists, false otherwise. + */ +native bool TFDB_IsValidRocket(int iIndex); + +/** + * Finds a rocket's internal index by its entity index. + * + * @param iEntity The rocket's entity index. + * @return The rocket's internal index, or -1 if not found. + */ +native int TFDB_FindRocketByEntity(int iEntity); + +/** + * Checks if the dodgeball gamemode is currently enabled. + * + * @return True if dodgeball is enabled, false otherwise. + */ +native bool TFDB_IsDodgeballEnabled(); + +/** + * Gets the entity index of a rocket. + * + * @param iIndex The rocket's internal index. + * @return The rocket's entity index. + */ +native int TFDB_GetRocketEntity(int iIndex); + +/** + * Gets the behaviour flags of a rocket instance. + * + * @param iIndex The rocket's internal index. + * @return The rocket's RocketFlags bitfield. + */ +native RocketFlags TFDB_GetRocketFlags(int iIndex); + +/** + * Sets the behaviour flags of a rocket instance. + * + * @param iIndex The rocket's internal index. + * @param iFlags The new RocketFlags bitfield. + */ +native void TFDB_SetRocketFlags(int iIndex, RocketFlags iFlags); + +/** + * Gets the current target of a rocket. + * + * @param iIndex The rocket's internal index. + * @return The target client index, or -1 if no target. + */ +native int TFDB_GetRocketTarget(int iIndex); + +/** + * Sets the target of a rocket. + * + * @param iIndex The rocket's internal index. + * @param iTarget The new target client index (as entity reference). + */ +native void TFDB_SetRocketTarget(int iIndex, int iTarget); + +/** + * Gets the deflection count for the current event (resets each round). + * + * @param iIndex The rocket's internal index. + * @return The number of deflections this event. + */ +native int TFDB_GetRocketEventDeflections(int iIndex); + +/** + * Sets the event deflection count. + * + * @param iIndex The rocket's internal index. + * @param iDeflections The new deflection count. + */ +native void TFDB_SetRocketEventDeflections(int iIndex, int iDeflections); + +/** + * Gets the total deflection count for this rocket's lifetime. + * + * @param iIndex The rocket's internal index. + * @return The total number of deflections. + */ +native int TFDB_GetRocketDeflections(int iIndex); + +/** + * Sets the total deflection count. + * + * @param iIndex The rocket's internal index. + * @param iDeflections The new total deflection count. + */ +native void TFDB_SetRocketDeflections(int iIndex, int iDeflections); + +/** + * Gets the class index of a rocket. + * + * @param iIndex The rocket's internal index. + * @return The rocket class index. + */ +native int TFDB_GetRocketClass(int iIndex); + +/** + * Sets the class of a rocket. + * + * @param iIndex The rocket's internal index. + * @param iClass The new rocket class index. + */ +native void TFDB_SetRocketClass(int iIndex, int iClass); + +// ============ Rocket Class Configuration ============ + +/** + * Gets the total number of rocket classes defined. + * + * @return The number of rocket classes. + */ +native int TFDB_GetRocketClassCount(); + +/** + * Gets the behaviour type of a rocket class. + * + * @param iClass The rocket class index. + * @return The BehaviourTypes value (Homing, LegacyHoming, etc). + */ +native BehaviourTypes TFDB_GetRocketClassBehaviour(int iClass); + +/** + * Sets the behaviour type of a rocket class. + * + * @param iClass The rocket class index. + * @param iBehaviour The new BehaviourTypes value. + */ +native void TFDB_SetRocketClassBehaviour(int iClass, BehaviourTypes iBehaviour); + +/** Gets the default flags of a rocket class. */ +native RocketFlags TFDB_GetRocketClassFlags(int iClass); + +/** Sets the default flags of a rocket class. */ +native void TFDB_SetRocketClassFlags(int iClass, RocketFlags iFlags); + +/** Gets the base damage of a rocket class. */ +native float TFDB_GetRocketClassDamage(int iClass); + +/** Sets the base damage of a rocket class. */ +native void TFDB_SetRocketClassDamage(int iClass, float fDamage); + +/** Gets the damage increment per deflection. */ +native float TFDB_GetRocketClassDamageIncrement(int iClass); + +/** Sets the damage increment per deflection. */ +native void TFDB_SetRocketClassDamageIncrement(int iClass, float fDamage); + +/** Gets the base speed (in Hammer units/second). */ +native float TFDB_GetRocketClassSpeed(int iClass); + +/** Sets the base speed (in Hammer units/second). */ +native void TFDB_SetRocketClassSpeed(int iClass, float fSpeed); + +/** Gets the speed increment per deflection. */ +native float TFDB_GetRocketClassSpeedIncrement(int iClass); + +/** Sets the speed increment per deflection. */ +native void TFDB_SetRocketClassSpeedIncrement(int iClass, float fSpeed); + +/** Gets the maximum speed limit (0 = no limit). */ +native float TFDB_GetRocketClassSpeedLimit(int iClass); + +/** Sets the maximum speed limit (0 = no limit). */ +native void TFDB_SetRocketClassSpeedLimit(int iClass, float fSpeed); + +/** Gets the base turn rate (degrees/second). */ +native float TFDB_GetRocketClassTurnRate(int iClass); + +/** Sets the base turn rate (degrees/second). */ +native void TFDB_SetRocketClassTurnRate(int iClass, float fTurnRate); + +/** Gets the turn rate increment per deflection. */ +native float TFDB_GetRocketClassTurnRateIncrement(int iClass); + +/** Sets the turn rate increment per deflection. */ +native void TFDB_SetRocketClassTurnRateIncrement(int iClass, float fTurnRate); + +/** Gets the maximum turn rate limit (0 = no limit). */ +native float TFDB_GetRocketClassTurnRateLimit(int iClass); + +/** Sets the maximum turn rate limit (0 = no limit). */ +native void TFDB_SetRocketClassTurnRateLimit(int iClass, float fTurnRate); + +/** Gets the elevation rate (vertical tracking speed). */ +native float TFDB_GetRocketClassElevationRate(int iClass); + +/** Sets the elevation rate (vertical tracking speed). */ +native void TFDB_SetRocketClassElevationRate(int iClass, float fElevation); + +/** Gets the maximum elevation limit. */ +native float TFDB_GetRocketClassElevationLimit(int iClass); + +/** Sets the maximum elevation limit. */ +native void TFDB_SetRocketClassElevationLimit(int iClass, float fElevation); + +/** Gets the rockets modifier (affects speed based on active rockets). */ +native float TFDB_GetRocketClassRocketsModifier(int iClass); + +/** Sets the rockets modifier (affects speed based on active rockets). */ +native void TFDB_SetRocketClassRocketsModifier(int iClass, float fModifier); + +/** Gets the player modifier (affects speed based on player count). */ +native float TFDB_GetRocketClassPlayerModifier(int iClass); + +/** Sets the player modifier (affects speed based on player count). */ +native void TFDB_SetRocketClassPlayerModifier(int iClass, float fModifier); + +/** Gets the control delay (time before player can redirect after deflect). */ +native float TFDB_GetRocketClassControlDelay(int iClass); + +/** Sets the control delay. */ +native void TFDB_SetRocketClassControlDelay(int iClass, float fDelay); + + +/** Sets the total number of rocket classes. */ +native void TFDB_SetRocketClassCount(int iCount); + +/** Sets the entity index of a rocket. */ +native void TFDB_SetRocketEntity(int iIndex, int iEntity); + +/** Gets the maximum number of bounces allowed. */ +native int TFDB_GetRocketClassMaxBounces(int iClass); + +/** Sets the maximum number of bounces allowed. */ +native void TFDB_SetRocketClassMaxBounces(int iClass, int iBounces); + +// ============ Spawner Configuration ============ + +/** Gets the name of a spawner class. */ +native void TFDB_GetSpawnersName(int iSpawnerClass, char[] strBuffer, int iMaxLen); + +/** Sets the name of a spawner class. */ +native void TFDB_SetSpawnersName(int iSpawnerClass, const char[] strName); + +/** Gets the maximum rockets a spawner can fire at once. */ +native int TFDB_GetSpawnersMaxRockets(int iSpawnerClass); + +/** Sets the maximum rockets a spawner can fire at once. */ +native void TFDB_SetSpawnersMaxRockets(int iSpawnerClass, int iMaxRockets); + +/** Gets the spawn interval (seconds between rockets). */ +native float TFDB_GetSpawnersInterval(int iSpawnerClass); + +/** Sets the spawn interval (seconds between rockets). */ +native void TFDB_SetSpawnersInterval(int iSpawnerClass, float fInterval); + +/** + * Gets the rocket class spawn chances table. + * @note You MUST call delete on the returned ArrayList Handle when finished, or your plugin will leak memory. + */ +native ArrayList TFDB_GetSpawnersChancesTable(int iSpawnerClass); + +/** Sets the rocket class spawn chances table. */ +native void TFDB_SetSpawnersChancesTable(int iSpawnerClass, ArrayList hTable); + +/** Gets the total number of spawner classes. */ +native int TFDB_GetSpawnersCount(); + +/** Sets the total number of spawner classes. */ +native void TFDB_SetSpawnersCount(int iCount); + +// ============ Spawn Points ============ + +/** Gets the current RED team spawn point index. */ +native int TFDB_GetCurrentRedSpawn(); + +/** Sets the current RED team spawn point index. */ +native void TFDB_SetCurrentRedSpawn(int iRedSpawn); + +/** Gets the number of RED team spawn points. */ +native int TFDB_GetSpawnPointsRedCount(); + +/** Sets the number of RED team spawn points. */ +native void TFDB_SetSpawnPointsRedCount(int iCount); + +/** Gets the spawner class assigned to a RED spawn point. */ +native int TFDB_GetSpawnPointsRedClass(int iSpawner); + +/** Sets the spawner class assigned to a RED spawn point. */ +native void TFDB_SetSpawnPointsRedClass(int iSpawner, int iSpawnerClass); + +/** Gets the entity index of a RED spawn point. */ +native int TFDB_GetSpawnPointsRedEntity(int iSpawner); + +/** Sets the entity index of a RED spawn point. */ +native void TFDB_SetSpawnPointsRedEntity(int iSpawner, int iEntity); + +/** Gets the current BLU team spawn point index. */ +native int TFDB_GetCurrentBluSpawn(); + +/** Sets the current BLU team spawn point index. */ +native void TFDB_SetCurrentBluSpawn(int iBluSpawn); + +/** Gets the number of BLU team spawn points. */ +native int TFDB_GetSpawnPointsBluCount(); + +/** Sets the number of BLU team spawn points. */ +native void TFDB_SetSpawnPointsBluCount(int iCount); + +/** Gets the spawner class assigned to a BLU spawn point. */ +native int TFDB_GetSpawnPointsBluClass(int iSpawner); + +/** Sets the spawner class assigned to a BLU spawn point. */ +native void TFDB_SetSpawnPointsBluClass(int iSpawner, int iSpawnerClass); + +/** Gets the entity index of a BLU spawn point. */ +native int TFDB_GetSpawnPointsBluEntity(int iSpawner); + +/** Sets the entity index of a BLU spawn point. */ +native void TFDB_SetSpawnPointsBluEntity(int iSpawner, int iEntity); + +// ============ Game State ============ + +/** Returns true if the round has started. */ +native bool TFDB_GetRoundStarted(); + +/** Gets the current round count. */ +native int TFDB_GetRoundCount(); + +/** Gets the total number of rockets fired this round. */ +native int TFDB_GetRocketsFired(); + +/** Gets the next scheduled rocket spawn time. */ +native float TFDB_GetNextSpawnTime(); + +/** Sets the next scheduled rocket spawn time. */ +native void TFDB_SetNextSpawnTime(float fSpawnTime); + +/** Gets the team of the last player to die. */ +native int TFDB_GetLastDeadTeam(); + +/** Gets the client index of the last player to die. */ +native int TFDB_GetLastDeadClient(); + +/** Gets the client index of the last player to steal a rocket. */ +native int TFDB_GetLastStealer(); + +// ============ Rocket Runtime State ============ + +/** Gets the current speed of a rocket (Hammer units/second). */ +native float TFDB_GetRocketSpeed(int iIndex); + +/** Sets the current speed of a rocket (Hammer units/second). */ +native void TFDB_SetRocketSpeed(int iIndex, float fSpeed); + +/** Gets the current speed of a rocket in MPH. */ +native float TFDB_GetRocketMphSpeed(int iIndex); + +/** Sets the current speed of a rocket in MPH. */ +native void TFDB_SetRocketMphSpeed(int iIndex, float fSpeed); + +/** Gets the current direction vector of a rocket. */ +native void TFDB_GetRocketDirection(int iIndex, float fBuffer[3]); + +/** Sets the direction vector of a rocket. */ +native void TFDB_SetRocketDirection(int iIndex, float fDirection[3]); + +/** Gets the timestamp of the last deflection. */ +native float TFDB_GetRocketLastDeflectionTime(int iIndex); + +/** Sets the timestamp of the last deflection. */ +native void TFDB_SetRocketLastDeflectionTime(int iIndex, float fTime); + +/** Gets the timestamp of the last beep sound. */ +native float TFDB_GetRocketLastBeepTime(int iIndex); + +/** Sets the timestamp of the last beep sound. */ +native void TFDB_SetRocketLastBeepTime(int iIndex, float fTime); + +/** Gets the total number of active rockets. */ +native int TFDB_GetRocketCount(); + +/** Gets the spawn timestamp of a rocket. */ +native float TFDB_GetLastSpawnTime(int iIndex); + +/** Gets the number of bounces for a rocket. */ +native int TFDB_GetRocketBounces(int iIndex); + +/** Sets the number of bounces for a rocket. */ +native void TFDB_SetRocketBounces(int iIndex, int iBounces); + +// ============ Rocket Class Strings & Sounds ============ + +/** Gets the short name of a rocket class. */ +native void TFDB_GetRocketClassName(int iClass, char[] strBuffer, int iMaxLen); + +/** Sets the short name of a rocket class. */ +native void TFDB_SetRocketClassName(int iClass, const char[] strName); + +/** Gets the display name of a rocket class. */ +native void TFDB_GetRocketClassLongName(int iClass, char[] strBuffer, int iMaxLen); + +/** Sets the display name of a rocket class. */ +native void TFDB_SetRocketClassLongName(int iClass, const char[] strName); + +/** Gets the model path of a rocket class. */ +native void TFDB_GetRocketClassModel(int iClass, char[] strBuffer, int iMaxLen); + +/** Sets the model path of a rocket class. */ +native void TFDB_SetRocketClassModel(int iClass, const char[] strPath); + +/** Gets the beep sound interval (seconds). */ +native float TFDB_GetRocketClassBeepInterval(int iClass); + +/** Sets the beep sound interval (seconds). */ +native void TFDB_SetRocketClassBeepInterval(int iClass, float fInterval); + +/** Gets the spawn sound path. */ +native void TFDB_GetRocketClassSpawnSound(int iClass, char[] strBuffer, int iMaxLen); + +/** Sets the spawn sound path. */ +native void TFDB_SetRocketClassSpawnSound(int iClass, const char[] strPath); + +/** Gets the beep sound path. */ +native void TFDB_GetRocketClassBeepSound(int iClass, char[] strBuffer, int iMaxLen); + +/** Sets the beep sound path. */ +native void TFDB_SetRocketClassBeepSound(int iClass, const char[] strPath); + +/** Gets the alert sound path. */ +native void TFDB_GetRocketClassAlertSound(int iClass, char[] strBuffer, int iMaxLen); + +/** Sets the alert sound path. */ +native void TFDB_SetRocketClassAlertSound(int iClass, const char[] strPath); + +/** Gets the critical hit chance (0.0 to 1.0). */ +native float TFDB_GetRocketClassCritChance(int iClass); + +/** Sets the critical hit chance (0.0 to 1.0). */ +native void TFDB_SetRocketClassCritChance(int iClass, float fChance); + +/** Gets the target weight for target selection. */ +native float TFDB_GetRocketClassTargetWeight(int iClass); + +/** Sets the target weight for target selection. */ +native void TFDB_SetRocketClassTargetWeight(int iClass, float fWeight); + +// ============ Command Handlers ============ + +/** + * Gets the commands to execute on rocket spawn. + * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. + */ +native DataPack TFDB_GetRocketClassCmdsOnSpawn(int iClass); + +/** Sets the commands to execute on rocket spawn. */ +native void TFDB_SetRocketClassCmdsOnSpawn(int iClass, DataPack hCmds); + +/** + * Gets the commands to execute on deflection. + * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. + */ +native DataPack TFDB_GetRocketClassCmdsOnDeflect(int iClass); + +/** Sets the commands to execute on deflection. */ +native void TFDB_SetRocketClassCmdsOnDeflect(int iClass, DataPack hCmds); + +/** + * Gets the commands to execute when rocket kills a player. + * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. + */ +native DataPack TFDB_GetRocketClassCmdsOnKill(int iClass); + +/** Sets the commands to execute when rocket kills a player. */ +native void TFDB_SetRocketClassCmdsOnKill(int iClass, DataPack hCmds); + +/** + * Gets the commands to execute when rocket explodes. + * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. + */ +native DataPack TFDB_GetRocketClassCmdsOnExplode(int iClass); + +/** Sets the commands to execute when rocket explodes. */ +native void TFDB_SetRocketClassCmdsOnExplode(int iClass, DataPack hCmds); + +/** + * Gets the commands to execute when rocket has no target. + * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. + */ +native DataPack TFDB_GetRocketClassCmdsOnNoTarget(int iClass); + +/** Sets the commands to execute when rocket has no target. */ +native void TFDB_SetRocketClassCmdsOnNoTarget(int iClass, DataPack hCmds); + +/** Gets the bounce velocity scale factor. */ +native float TFDB_GetRocketClassBounceScale(int iClass); + +/** Sets the bounce velocity scale factor. */ +native void TFDB_SetRocketClassBounceScale(int iClass, float fScale); + +// ============ Actions ============ + +/** + * Creates and spawns a new rocket. + * + * @param iSpawnerEntity The spawner entity to spawn from. + * @param iSpawnerClass The spawner class index. + * @param iTeam The team the rocket belongs to. + * @param iClass Optional rocket class (-1 for random from spawner chances). + */ +native void TFDB_CreateRocket(int iSpawnerEntity, int iSpawnerClass, int iTeam, int iClass = -1); + +/** Destroys a single rocket by index. */ +native void TFDB_DestroyRocket(int iIndex); + +/** Destroys all active rockets. */ +native void TFDB_DestroyRockets(); + +/** Clears all rocket class definitions. */ +native void TFDB_DestroyRocketClasses(); + +/** Clears all spawner class definitions. */ +native void TFDB_DestroySpawners(); + +/** + * Parses a configuration file and loads rocket/spawner classes. + * + * @param strConfigFile Config file name in configs/dodgeball/ (default: "general.cfg") + */ +native void TFDB_ParseConfigurations(const char[] strConfigFile = "general.cfg"); + +/** Populates spawn points from the current map. */ +native void TFDB_PopulateSpawnPoints(); + +/** Runs the homing rocket think logic for a rocket. */ +native void TFDB_HomingRocketThink(int iIndex); + +/** Runs the legacy homing rocket think logic for a rocket. */ +native void TFDB_RocketLegacyThink(int iIndex); + +/** Gets the state flags of a rocket. */ +native RocketState TFDB_GetRocketState(int iIndex); + +/** Sets the state flags of a rocket. */ +native void TFDB_SetRocketState(int iIndex, RocketState iState); + +/** + * Gets steal info for a client. + * + * @param iClient Client index. + * @param stole Output: whether client stole a rocket this round. + * @param count Output: number of rockets stolen this round. + */ +native void TFDB_GetStealInfo(int iClient, bool &stole, int &count); + +/** + * Sets steal info for a client. + * + * @param iClient Client index. + * @param stole Whether client stole a rocket. + * @param count Number of rockets stolen. + */ +native void TFDB_SetStealInfo(int iClient, bool stole, int count); + +// ============ Preset Functions ============ + +/** + * Gets the number of loaded presets. + * + * @return Number of presets (0 if none loaded or presets.cfg missing). + */ +native int TFDB_GetPresetCount(); + +/** + * Gets the display name of a preset. + * + * @param iPreset Preset index (0 to TFDB_GetPresetCount()-1). + * @param buffer Buffer to store the name. + * @param maxlen Maximum buffer length. + */ +native void TFDB_GetPresetName(int iPreset, char[] buffer, int maxlen); + +/** + * Applies a preset: destroys active rockets, sets spawner max rockets, + * spawn interval, and chances table to 100% the preset's rocket class. + * + * @param iPreset Preset index (0 to TFDB_GetPresetCount()-1). + * @return True if applied successfully, false if invalid preset or class not found. + */ +native bool TFDB_ApplyPreset(int iPreset); + +public SharedPlugin __pl_TFDB = +{ + name = "tfdb", + file = "TF2Dodgeball.smx", + + #if defined REQUIRE_PLUGIN + required=1, + #else + required=0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_TFDB_SetNTVOptional() +{ + MarkNativeAsOptional("TFDB_IsValidRocket"); + MarkNativeAsOptional("TFDB_FindRocketByEntity"); + MarkNativeAsOptional("TFDB_IsDodgeballEnabled"); + MarkNativeAsOptional("TFDB_GetRocketEntity"); + MarkNativeAsOptional("TFDB_GetRocketFlags"); + MarkNativeAsOptional("TFDB_SetRocketFlags"); + MarkNativeAsOptional("TFDB_GetRocketTarget"); + MarkNativeAsOptional("TFDB_SetRocketTarget"); + MarkNativeAsOptional("TFDB_GetRocketEventDeflections"); + MarkNativeAsOptional("TFDB_SetRocketEventDeflections"); + MarkNativeAsOptional("TFDB_GetRocketDeflections"); + MarkNativeAsOptional("TFDB_SetRocketDeflections"); + MarkNativeAsOptional("TFDB_GetRocketClass"); + MarkNativeAsOptional("TFDB_SetRocketClass"); + MarkNativeAsOptional("TFDB_GetRocketClassCount"); + MarkNativeAsOptional("TFDB_GetRocketClassBehaviour"); + MarkNativeAsOptional("TFDB_SetRocketClassBehaviour"); + MarkNativeAsOptional("TFDB_GetRocketClassFlags"); + MarkNativeAsOptional("TFDB_SetRocketClassFlags"); + MarkNativeAsOptional("TFDB_GetRocketClassDamage"); + MarkNativeAsOptional("TFDB_SetRocketClassDamage"); + MarkNativeAsOptional("TFDB_GetRocketClassDamageIncrement"); + MarkNativeAsOptional("TFDB_SetRocketClassDamageIncrement"); + MarkNativeAsOptional("TFDB_GetRocketClassSpeed"); + MarkNativeAsOptional("TFDB_SetRocketClassSpeed"); + MarkNativeAsOptional("TFDB_GetRocketClassSpeedIncrement"); + MarkNativeAsOptional("TFDB_SetRocketClassSpeedIncrement"); + MarkNativeAsOptional("TFDB_GetRocketClassSpeedLimit"); + MarkNativeAsOptional("TFDB_SetRocketClassSpeedLimit"); + MarkNativeAsOptional("TFDB_GetRocketClassTurnRate"); + MarkNativeAsOptional("TFDB_SetRocketClassTurnRate"); + MarkNativeAsOptional("TFDB_GetRocketClassTurnRateIncrement"); + MarkNativeAsOptional("TFDB_SetRocketClassTurnRateIncrement"); + MarkNativeAsOptional("TFDB_GetRocketClassTurnRateLimit"); + MarkNativeAsOptional("TFDB_SetRocketClassTurnRateLimit"); + MarkNativeAsOptional("TFDB_GetRocketClassElevationRate"); + MarkNativeAsOptional("TFDB_SetRocketClassElevationRate"); + MarkNativeAsOptional("TFDB_GetRocketClassElevationLimit"); + MarkNativeAsOptional("TFDB_SetRocketClassElevationLimit"); + MarkNativeAsOptional("TFDB_GetRocketClassRocketsModifier"); + MarkNativeAsOptional("TFDB_SetRocketClassRocketsModifier"); + MarkNativeAsOptional("TFDB_GetRocketClassPlayerModifier"); + MarkNativeAsOptional("TFDB_SetRocketClassPlayerModifier"); + MarkNativeAsOptional("TFDB_GetRocketClassControlDelay"); + MarkNativeAsOptional("TFDB_SetRocketClassControlDelay"); + MarkNativeAsOptional("TFDB_SetRocketClassCount"); + MarkNativeAsOptional("TFDB_SetRocketEntity"); + MarkNativeAsOptional("TFDB_GetRocketClassMaxBounces"); + MarkNativeAsOptional("TFDB_SetRocketClassMaxBounces"); + MarkNativeAsOptional("TFDB_GetSpawnersName"); + MarkNativeAsOptional("TFDB_SetSpawnersName"); + MarkNativeAsOptional("TFDB_GetSpawnersMaxRockets"); + MarkNativeAsOptional("TFDB_SetSpawnersMaxRockets"); + MarkNativeAsOptional("TFDB_GetSpawnersInterval"); + MarkNativeAsOptional("TFDB_SetSpawnersInterval"); + MarkNativeAsOptional("TFDB_GetSpawnersChancesTable"); + MarkNativeAsOptional("TFDB_SetSpawnersChancesTable"); + MarkNativeAsOptional("TFDB_GetSpawnersCount"); + MarkNativeAsOptional("TFDB_SetSpawnersCount"); + MarkNativeAsOptional("TFDB_GetCurrentRedSpawn"); + MarkNativeAsOptional("TFDB_SetCurrentRedSpawn"); + MarkNativeAsOptional("TFDB_GetSpawnPointsRedCount"); + MarkNativeAsOptional("TFDB_SetSpawnPointsRedCount"); + MarkNativeAsOptional("TFDB_GetSpawnPointsRedClass"); + MarkNativeAsOptional("TFDB_SetSpawnPointsRedClass"); + MarkNativeAsOptional("TFDB_GetSpawnPointsRedEntity"); + MarkNativeAsOptional("TFDB_SetSpawnPointsRedEntity"); + MarkNativeAsOptional("TFDB_GetCurrentBluSpawn"); + MarkNativeAsOptional("TFDB_SetCurrentBluSpawn"); + MarkNativeAsOptional("TFDB_GetSpawnPointsBluCount"); + MarkNativeAsOptional("TFDB_SetSpawnPointsBluCount"); + MarkNativeAsOptional("TFDB_GetSpawnPointsBluClass"); + MarkNativeAsOptional("TFDB_SetSpawnPointsBluClass"); + MarkNativeAsOptional("TFDB_GetSpawnPointsBluEntity"); + MarkNativeAsOptional("TFDB_SetSpawnPointsBluEntity"); + MarkNativeAsOptional("TFDB_GetRoundStarted"); + MarkNativeAsOptional("TFDB_GetRoundCount"); + MarkNativeAsOptional("TFDB_GetRocketsFired"); + MarkNativeAsOptional("TFDB_GetNextSpawnTime"); + MarkNativeAsOptional("TFDB_SetNextSpawnTime"); + MarkNativeAsOptional("TFDB_GetLastDeadTeam"); + MarkNativeAsOptional("TFDB_GetLastDeadClient"); + MarkNativeAsOptional("TFDB_GetLastStealer"); + MarkNativeAsOptional("TFDB_GetRocketSpeed"); + MarkNativeAsOptional("TFDB_SetRocketSpeed"); + MarkNativeAsOptional("TFDB_GetRocketMphSpeed"); + MarkNativeAsOptional("TFDB_SetRocketMphSpeed"); + MarkNativeAsOptional("TFDB_GetRocketDirection"); + MarkNativeAsOptional("TFDB_SetRocketDirection"); + MarkNativeAsOptional("TFDB_GetRocketLastDeflectionTime"); + MarkNativeAsOptional("TFDB_SetRocketLastDeflectionTime"); + MarkNativeAsOptional("TFDB_GetRocketLastBeepTime"); + MarkNativeAsOptional("TFDB_SetRocketLastBeepTime"); + MarkNativeAsOptional("TFDB_GetRocketCount"); + MarkNativeAsOptional("TFDB_GetLastSpawnTime"); + MarkNativeAsOptional("TFDB_GetRocketBounces"); + MarkNativeAsOptional("TFDB_SetRocketBounces"); + MarkNativeAsOptional("TFDB_GetRocketClassName"); + MarkNativeAsOptional("TFDB_SetRocketClassName"); + MarkNativeAsOptional("TFDB_GetRocketClassLongName"); + MarkNativeAsOptional("TFDB_SetRocketClassLongName"); + MarkNativeAsOptional("TFDB_GetRocketClassModel"); + MarkNativeAsOptional("TFDB_SetRocketClassModel"); + MarkNativeAsOptional("TFDB_GetRocketClassBeepInterval"); + MarkNativeAsOptional("TFDB_SetRocketClassBeepInterval"); + MarkNativeAsOptional("TFDB_GetRocketClassSpawnSound"); + MarkNativeAsOptional("TFDB_SetRocketClassSpawnSound"); + MarkNativeAsOptional("TFDB_GetRocketClassBeepSound"); + MarkNativeAsOptional("TFDB_SetRocketClassBeepSound"); + MarkNativeAsOptional("TFDB_GetRocketClassAlertSound"); + MarkNativeAsOptional("TFDB_SetRocketClassAlertSound"); + MarkNativeAsOptional("TFDB_GetRocketClassCritChance"); + MarkNativeAsOptional("TFDB_SetRocketClassCritChance"); + MarkNativeAsOptional("TFDB_GetRocketClassTargetWeight"); + MarkNativeAsOptional("TFDB_SetRocketClassTargetWeight"); + MarkNativeAsOptional("TFDB_GetRocketClassCmdsOnSpawn"); + MarkNativeAsOptional("TFDB_SetRocketClassCmdsOnSpawn"); + MarkNativeAsOptional("TFDB_GetRocketClassCmdsOnDeflect"); + MarkNativeAsOptional("TFDB_SetRocketClassCmdsOnDeflect"); + MarkNativeAsOptional("TFDB_GetRocketClassCmdsOnKill"); + MarkNativeAsOptional("TFDB_SetRocketClassCmdsOnKill"); + MarkNativeAsOptional("TFDB_GetRocketClassCmdsOnExplode"); + MarkNativeAsOptional("TFDB_SetRocketClassCmdsOnExplode"); + MarkNativeAsOptional("TFDB_GetRocketClassCmdsOnNoTarget"); + MarkNativeAsOptional("TFDB_SetRocketClassCmdsOnNoTarget"); + MarkNativeAsOptional("TFDB_GetRocketClassBounceScale"); + MarkNativeAsOptional("TFDB_SetRocketClassBounceScale"); + MarkNativeAsOptional("TFDB_CreateRocket"); + MarkNativeAsOptional("TFDB_DestroyRocket"); + MarkNativeAsOptional("TFDB_DestroyRockets"); + MarkNativeAsOptional("TFDB_DestroyRocketClasses"); + MarkNativeAsOptional("TFDB_DestroySpawners"); + MarkNativeAsOptional("TFDB_ParseConfigurations"); + MarkNativeAsOptional("TFDB_PopulateSpawnPoints"); + MarkNativeAsOptional("TFDB_HomingRocketThink"); + MarkNativeAsOptional("TFDB_RocketLegacyThink"); + MarkNativeAsOptional("TFDB_GetRocketState"); + MarkNativeAsOptional("TFDB_SetRocketState"); + MarkNativeAsOptional("TFDB_GetStealInfo"); + MarkNativeAsOptional("TFDB_SetStealInfo"); + MarkNativeAsOptional("TFDB_GetPresetCount"); + MarkNativeAsOptional("TFDB_GetPresetName"); + MarkNativeAsOptional("TFDB_ApplyPreset"); +} +#endif \ No newline at end of file diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_airblast_prevention.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_airblast_prevention.sp new file mode 100644 index 00000000..b3def29b --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_airblast_prevention.sp @@ -0,0 +1,185 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +#include + +#define PLUGIN_NAME "[TFDB] Airblast push prevention" +#define PLUGIN_AUTHOR "BloodyNightmare, Mitchell, edited by x07x08" +#define PLUGIN_DESCRIPTION "Prevents users from pushing each other by airblasting." +#define PLUGIN_VERSION "1.2.2" +#define PLUGIN_URL "https://github.com/x07x08/TF2-Dodgeball-Modified" + +ConVar CvarCommandEnabled; +bool ClientEnabled[MAXPLAYERS + 1] = {true, ...}; +bool Loaded; + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_URL +}; + +public void OnPluginStart() +{ + LoadTranslations("tfdb.phrases.txt"); + + CvarCommandEnabled = CreateConVar("tf_dodgeball_ab_command", "1", "Allow people to toggle airblast prevention?\n Turning this off also enables airblast prevention on all clients.", _, true, 0.0, true, 1.0); + + RegConsoleCmd("sm_ab", CmdAirblastPrevention, "Toggles push prevention."); + + if (!TFDB_IsDodgeballEnabled()) return; + + TFDB_OnRocketsConfigExecuted("general.cfg"); + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient)) continue; + + SDKHook(iClient, SDKHook_WeaponCanUse, WeaponCanUseCallback); + + // Enabled by default + SetEntityFlags(iClient, GetEntityFlags(iClient) | FL_NOTARGET); + } +} + +public void TFDB_OnRocketsConfigExecuted(const char[] strConfigFile) +{ + if (Loaded) return; + + HookEvent("player_spawn", OnPlayerSpawn); + HookEvent("player_death", OnPlayerDeath); + + CvarCommandEnabled.AddChangeHook(CvarCommandCallback); + + Loaded = true; +} + +public void OnMapEnd() +{ + if (!Loaded) return; + + UnhookEvent("player_spawn", OnPlayerSpawn); + UnhookEvent("player_death", OnPlayerDeath); + + CvarCommandEnabled.RemoveChangeHook(CvarCommandCallback); + + Loaded = false; +} + +public void OnPlayerSpawn(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iClient = GetClientOfUserId(hEvent.GetInt("userid")); + + if (ClientEnabled[iClient]) + { + // Sound issue. + SetEntityFlags(iClient, GetEntityFlags(iClient) | FL_NOTARGET); + } +} + +public void OnPlayerDeath(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iClient = GetClientOfUserId(hEvent.GetInt("userid")); + + // If you spectate an enemy, he will "airblast" you. + // Forcefully adding FL_NOTARGET seems to fix the problem. + SetEntityFlags(iClient, GetEntityFlags(iClient) | FL_NOTARGET); +} + +public Action CmdAirblastPrevention(int iClient, int iArgs) +{ + if (iClient == 0) + { + ReplyToCommand(iClient, "Command is in-game only."); + + return Plugin_Handled; + } + + if (!TFDB_IsDodgeballEnabled() || !CvarCommandEnabled.BoolValue) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + ClientEnabled[iClient] = !ClientEnabled[iClient]; + + if (IsPlayerAlive(iClient)) + { + ToggleAirblastPrevention(iClient); + } + + CReplyToCommand(iClient, "%t", ClientEnabled[iClient] ? "Dodgeball_AirblastPreventionCmd_Enabled" : "Dodgeball_AirblastPreventionCmd_Disabled"); + + return Plugin_Handled; +} + +public void OnClientDisconnect(int iClient) +{ + ClientEnabled[iClient] = false; +} + +public void OnClientConnected(int iClient) +{ + ClientEnabled[iClient] = true; +} + +public void OnClientPutInServer(int iClient) +{ + if (!Loaded) return; + + SDKHook(iClient, SDKHook_WeaponCanUse, WeaponCanUseCallback); +} + +// https://forums.alliedmods.net/showthread.php?t=265707 +// SDKHook_WeaponCanUse fires for any weapon change (including pickup) + +public Action WeaponCanUseCallback(int iClient, int iWeapon) +{ + int iFlags = GetEntityFlags(iClient); + + if (!(iFlags & FL_NOTARGET)) return Plugin_Continue; + + SetEntityFlags(iClient, iFlags & ~FL_NOTARGET); + + RequestFrame(ResetAirblastPrevention, GetClientUserId(iClient)); + + return Plugin_Continue; +} + +public void ResetAirblastPrevention(any iUserId) +{ + int iClient = GetClientOfUserId(iUserId); + + if (iClient == 0 || !IsClientInGame(iClient) || !ClientEnabled[iClient]) return; + + SetEntityFlags(iClient, GetEntityFlags(iClient) | FL_NOTARGET); +} + +void ToggleAirblastPrevention(int iClient) +{ + int iFlags = GetEntityFlags(iClient); + + SetEntityFlags(iClient, ClientEnabled[iClient] ? iFlags | FL_NOTARGET : iFlags & ~FL_NOTARGET); +} + +public void CvarCommandCallback(ConVar hConVar, const char[] strOldValue, const char[] strNewValue) +{ + if (hConVar.BoolValue) return; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient)) continue; + + ClientEnabled[iClient] = true; + + if (IsPlayerAlive(iClient)) SetEntityFlags(iClient, GetEntityFlags(iClient) | FL_NOTARGET); + } +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_ffa.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_ffa.sp new file mode 100644 index 00000000..59cd25c9 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_ffa.sp @@ -0,0 +1,615 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +#include + +#define PLUGIN_NAME "[TFDB] Free-for-All" +#define PLUGIN_AUTHOR "x07x08" +#define PLUGIN_DESCRIPTION "Makes all rockets neutral" +#define PLUGIN_VERSION "1.1.3" +#define PLUGIN_URL "https://github.com/x07x08/TF2-Dodgeball-Modified" + +bool Loaded; +bool FFAEnabled; +int BotCount; +bool VoteAllowed; +float LastVoteTime; +int OldTeam[MAXPLAYERS + 1]; + +Address MyWearables; + +ConVar CvarDisableOnBot; +ConVar CvarVoteTimeout; +ConVar CvarVoteDuration; +ConVar CvarToggleMode; +ConVar CvarAllowStealing; +ConVar CvarDisableConfig; +ConVar CvarEnableConfig; +ConVar CvarSwitchTeams; +ConVar CvarFriendlyFire; + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_URL +}; + +public void OnPluginStart() +{ + LoadTranslations("tfdb.phrases.txt"); + + MyWearables = view_as
(FindSendPropInfo("CTFPlayer", "m_hMyWearables")); + + CvarDisableOnBot = CreateConVar("tf_dodgeball_ffa_bot", "1", "Disable FFA when a bot joins?", _, true, 0.0, true, 1.0); + CvarVoteTimeout = CreateConVar("tf_dodgeball_ffa_timeout", "150", "Vote timeout (in seconds)", _, true, 0.0); + CvarVoteDuration = CreateConVar("tf_dodgeball_ffa_duration", "20", "Vote duration (in seconds)", _, true, 0.0); + CvarToggleMode = CreateConVar("tf_dodgeball_ffa_mode", "1", "How does changing FFA affect the rockets?\n 0 - No effect, wait for the next spawn\n 1 - Destroy all active rockets\n 2 - Immediately change the rockets to be neutral", _, true, 0.0); + CvarAllowStealing = CreateConVar("tf_dodgeball_ffa_stealing", "1", "Allow stealing in FFA mode?", _, true, 0.0, true, 1.0); + CvarDisableConfig = CreateConVar("tf_dodgeball_ffa_disablecfg", "sourcemod/dodgeball_ffa_disable.cfg", "Config file to execute when disabling FFA mode"); + CvarEnableConfig = CreateConVar("tf_dodgeball_ffa_enablecfg", "sourcemod/dodgeball_ffa_enable.cfg", "Config file to execute when enabling FFA mode"); + CvarSwitchTeams = CreateConVar("tf_dodgeball_ffa_teams", "1", "Automatically swap players when a team is empty in FFA mode?", _, true, 0.0, true, 1.0); + CvarFriendlyFire = FindConVar("mp_friendlyfire"); + + RegAdminCmd("sm_ffa", CmdToggleFFA, ADMFLAG_CONFIG, "Forcefully toggle FFA"); + RegConsoleCmd("sm_voteffa", CmdVoteFFA, "Start a vote to toggle FFA"); + + if (!TFDB_IsDodgeballEnabled()) return; + + TFDB_OnRocketsConfigExecuted("general.cfg"); +} + +public void TFDB_OnRocketsConfigExecuted(const char[] strConfigFile) +{ + if (Loaded) return; + + int iTeam; + + VoteAllowed = true; + FFAEnabled = false; + BotCount = 0; + LastVoteTime = 0.0; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient)) continue; + + iTeam = GetClientTeam(iClient); + + if (!(iTeam >= 2)) continue; + + OldTeam[iClient] = iTeam; + + if (IsFakeClient(iClient)) BotCount++; + } + + CvarDisableOnBot.AddChangeHook(DisableOnBotCallback); + + HookEvent("player_team", OnPlayerTeam); + HookEvent("player_death", OnPlayerDeath); + HookEvent("teamplay_round_start", OnRoundStart); + + Loaded = true; +} + +public void OnMapEnd() +{ + if (!Loaded) return; + + UnhookEvent("player_team", OnPlayerTeam); + UnhookEvent("player_death", OnPlayerDeath); + UnhookEvent("teamplay_round_start", OnRoundStart); + + CvarDisableOnBot.RemoveChangeHook(DisableOnBotCallback); + + VoteAllowed = false; + FFAEnabled = false; + BotCount = 0; + LastVoteTime = 0.0; + + CvarFriendlyFire.RestoreDefault(); + ExecuteDisableConfig(); + + Loaded = false; +} + +public void OnClientDisconnect(int iClient) +{ + OldTeam[iClient] = 0; + + if (!FFAEnabled || + !CvarSwitchTeams.BoolValue || + (CvarDisableOnBot.BoolValue && BotCount) || + !TFDB_GetRoundStarted()) + { + return; + } + + int iTeam = GetClientTeam(iClient); + + if (iTeam <= 1) return; + + int iOtherTeam = GetAnalogueTeam(iTeam); + + if (((GetTeamAliveClientCount(iTeam) - view_as(IsPlayerAlive(iClient))) == 0) && + ((GetTeamAliveClientCount(iOtherTeam) - 1) >= 1)) + { + ChangeAliveClientTeam(GetRandomTeamAliveClient(iOtherTeam), iTeam); + } +} + +public void OnClientConnected(int iClient) +{ + OldTeam[iClient] = 0; +} + +public void OnPlayerTeam(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iClient = GetClientOfUserId(hEvent.GetInt("userid")); + int iTeam = hEvent.GetInt("team"); + int iOldTeam = hEvent.GetInt("oldteam"); + + if (!FFAEnabled || + !CvarSwitchTeams.BoolValue || + (CvarDisableOnBot.BoolValue && BotCount) || + !TFDB_GetRoundStarted()) + { + OldTeam[iClient] = iTeam; + } + else + { + // If you swap between RED and BLU, this event gets fired first instead of player_death. + // This makes GetClientTeam report the new team instead of the old one when used inside a player_death callback. + + if (iTeam <= 1) + { + OldTeam[iClient] = iTeam; + } + else if ((iOldTeam >= 2) && + ((GetTeamAliveClientCount(iOldTeam) - view_as(IsPlayerAlive(iClient))) == 0) && + ((GetTeamAliveClientCount(iTeam) - 1) >= 1)) + { + ChangeAliveClientTeam(GetRandomTeamAliveClient(iTeam), iOldTeam); + } + } + + if (!IsFakeClient(iClient)) return; + + if ((iOldTeam <= 1) && (iTeam >= 2)) + { + BotCount++; + + if (FFAEnabled && (BotCount == 1) && CvarDisableOnBot.BoolValue) + { + CPrintToChatAll("%t", "Dodgeball_FFABot_Joined"); + CvarFriendlyFire.RestoreDefault(); + ExecuteDisableConfig(); + } + } + else if ((iOldTeam >= 2) && (iTeam <= 1)) + { + BotCount--; + + if (FFAEnabled && (BotCount == 0) && CvarDisableOnBot.BoolValue) + { + CPrintToChatAll("%t", "Dodgeball_FFABot_Left"); + CvarFriendlyFire.SetBool(true); + ExecuteEnableConfig(); + } + } +} + +public void OnPlayerDeath(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + if (!FFAEnabled || + !CvarSwitchTeams.BoolValue || + (CvarDisableOnBot.BoolValue && BotCount) || + !TFDB_GetRoundStarted()) + { + return; + } + + int iVictim = GetClientOfUserId(hEvent.GetInt("userid")); + + int iTeam = GetClientTeam(iVictim); + + if (iTeam <= 1) return; // ... + + int iOtherTeam = GetAnalogueTeam(iTeam); + + // Checking the alive players count in here doesn't exclude the player that has just died. + // Doing this check in a SDKHook_OnTakeDamagePost callback excludes him for some reason... + + if (((GetTeamAliveClientCount(iTeam) - 1) == 0) && ((GetTeamAliveClientCount(iOtherTeam) - 1) >= 1)) + { + ChangeAliveClientTeam(GetRandomTeamAliveClient(iOtherTeam), iTeam); + } +} + +public void OnRoundStart(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + if (!FFAEnabled || + !CvarSwitchTeams.BoolValue || + (CvarDisableOnBot.BoolValue && BotCount)) + { + return; + } + + int iTeam; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || ((iTeam = GetClientTeam(iClient)) <= 1)) continue; + + if ((OldTeam[iClient] >= 2) && + (OldTeam[iClient] != iTeam) && + ((GetTeamAliveClientCount(iTeam) - view_as(IsPlayerAlive(iClient))) >= 1)) + { + ChangeClientTeam(iClient, OldTeam[iClient]); + } + + if (OldTeam[iClient] <= 1) OldTeam[iClient] = iTeam; + } +} + +public Action CmdToggleFFA(int iClient, int iArgs) +{ + if (!TFDB_IsDodgeballEnabled()) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + ToggleFFA(); + + return Plugin_Handled; +} + +public Action CmdVoteFFA(int iClient, int iArgs) +{ + if (iClient == 0) + { + // CReplyToCommand prints the message twice... + ReplyToCommand(iClient, "Command is in-game only."); + + return Plugin_Handled; + } + + if (!TFDB_IsDodgeballEnabled()) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + if (IsVoteInProgress()) + { + CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); + + return Plugin_Handled; + } + + if (VoteAllowed) + { + VoteAllowed = false; + LastVoteTime = GetGameTime(); + + StartFFAVote(); + CreateTimer(CvarVoteTimeout.FloatValue, VoteTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); + } + else + { + CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Cooldown", + RoundToCeil((LastVoteTime + CvarVoteTimeout.FloatValue) - GetGameTime())); + } + + return Plugin_Handled; +} + +public void DisableOnBotCallback(ConVar hConvar, const char[] strOldValue, const char[] strNewValue) +{ + if (!FFAEnabled || !BotCount) return; + + if (hConvar.BoolValue) + { + CvarFriendlyFire.RestoreDefault(); + ExecuteDisableConfig(); + } + else + { + CvarFriendlyFire.SetBool(true); + ExecuteEnableConfig(); + } +} + +void StartFFAVote() +{ + char strMode[16]; + strMode = !FFAEnabled ? "Enable" : "Disable"; + + Menu hMenu = new Menu(VoteMenuHandler); + hMenu.VoteResultCallback = VoteResultHandler; + + hMenu.SetTitle("%s FFA mode?", strMode); + + hMenu.AddItem("0", "Yes"); + hMenu.AddItem("1", "No"); + + int iTotal; + int[] iClients = new int[MaxClients]; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) + { + continue; + } + + iClients[iTotal++] = iClient; + } + + hMenu.DisplayVote(iClients, iTotal, CvarVoteDuration.IntValue); +} + +public int VoteMenuHandler(Menu hMenu, MenuAction iMenuActions, int iParam1, int iParam2) +{ + switch (iMenuActions) + { + case MenuAction_End : + { + delete hMenu; + } + } + + return 0; +} + +public void VoteResultHandler(Menu hMenu, + int iNumVotes, + int iNumClients, + const int[][] iClientInfo, + int iNumItems, + const int[][] iItemInfo) +{ + int iWinnerIndex = 0; + + if (iNumItems > 1 && + (iItemInfo[0][VOTEINFO_ITEM_VOTES] == iItemInfo[1][VOTEINFO_ITEM_VOTES])) + { + iWinnerIndex = GetRandomInt(0, 1); + } + + char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); + + if (StrEqual(strWinner, "0")) + { + ToggleFFA(); + } + else + { + CPrintToChatAll("%t", "Dodgeball_FFAVote_Failed"); + } +} + +void EnableFFA() +{ + FFAEnabled = true; + + if (CvarDisableOnBot.BoolValue && BotCount) + { + CPrintToChatAll("%t", "Dodgeball_FFAVote_LateEnabled"); + } + else + { + CvarFriendlyFire.SetBool(true); + ExecuteEnableConfig(); + + switch (CvarToggleMode.IntValue) + { + case 1 : + { + TFDB_DestroyRockets(); + } + + case 2 : + { + ChangeRockets(); + } + } + + CPrintToChatAll("%t", "Dodgeball_FFAVote_Enabled"); + } +} + +void DisableFFA() +{ + FFAEnabled = false; + CvarFriendlyFire.RestoreDefault(); + ExecuteDisableConfig(); + + switch (CvarToggleMode.IntValue) + { + case 1 : + { + TFDB_DestroyRockets(); + } + + case 2 : + { + ChangeRockets(); + } + } + + CPrintToChatAll("%t", "Dodgeball_FFAVote_Disabled"); +} + +void ToggleFFA() +{ + if (!FFAEnabled) + { + EnableFFA(); + } + else + { + DisableFFA(); + } +} + +void ChangeRockets() +{ + RocketFlags iFlags, iClassFlags; + int iEntity; + + for (int iIndex = 0; iIndex < MAX_ROCKETS; iIndex++) + { + if (!TFDB_IsValidRocket(iIndex)) continue; + + iFlags = TFDB_GetRocketFlags(iIndex); + iClassFlags = TFDB_GetRocketClassFlags(TFDB_GetRocketClass(iIndex)); + iEntity = EntRefToEntIndex(TFDB_GetRocketEntity(iIndex)); + + if (FFAEnabled) + { + iFlags |= RocketFlag_IsNeutral; + + if (CvarAllowStealing.BoolValue) iFlags |= RocketFlag_CanBeStolen; + + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); + + TFDB_SetRocketFlags(iIndex, iFlags); + } + else + { + if (!(iClassFlags & RocketFlag_IsNeutral)) iFlags &= ~RocketFlag_IsNeutral; + + if (CvarAllowStealing.BoolValue && !(iClassFlags & RocketFlag_CanBeStolen)) iFlags &= ~RocketFlag_CanBeStolen; + + int iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", GetClientTeam(iOwner), 1); + + TFDB_SetRocketFlags(iIndex, iFlags); + } + } +} + +public Action VoteTimeoutCallback(Handle hTimer) +{ + VoteAllowed = true; + + return Plugin_Continue; +} + +public Action TFDB_OnRocketCreatedPre(int iIndex, int &iClass, RocketFlags &iFlags) +{ + if (FFAEnabled && (!CvarDisableOnBot.BoolValue || !BotCount)) + { + iFlags |= RocketFlag_IsNeutral; + + if (CvarAllowStealing.BoolValue) iFlags |= RocketFlag_CanBeStolen; + + return Plugin_Changed; + } + + return Plugin_Continue; +} + +void ExecuteDisableConfig() +{ + char strConfigPath[64]; CvarDisableConfig.GetString(strConfigPath, sizeof(strConfigPath)); + ServerCommand("exec \"%s\"", strConfigPath); +} + +void ExecuteEnableConfig() +{ + char strConfigPath[64]; CvarEnableConfig.GetString(strConfigPath, sizeof(strConfigPath)); + ServerCommand("exec \"%s\"", strConfigPath); +} + +int GetTeamAliveClientCount(int iTeam) +{ + int iCount; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient)) continue; + + if ((GetClientTeam(iClient) == iTeam) && IsPlayerAlive(iClient)) iCount++; + } + + return iCount; +} + +stock int GetAnalogueTeam(int iTeam) +{ + if (iTeam == view_as(TFTeam_Red)) return view_as(TFTeam_Blue); + + return view_as(TFTeam_Red); +} + +// https://forums.alliedmods.net/showthread.php?t=286924 + +int GetRandomTeamAliveClient(int iTeam) +{ + int[] iClients = new int[MaxClients]; + int iCount; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient)) continue; + + if ((GetClientTeam(iClient) == iTeam) && IsPlayerAlive(iClient)) iClients[iCount++] = iClient; + } + + return iCount == 0 ? -1 : iClients[GetRandomInt(0, iCount - 1)]; +} + +// https://forums.alliedmods.net/showthread.php?t=314271 + +void ChangeAliveClientTeam(int iClient, int iTeam) +{ + int iLifeState = GetEntProp(iClient, Prop_Send, "m_lifeState"); + SetEntProp(iClient, Prop_Send, "m_lifeState", 2); + + ChangeClientTeam(iClient, iTeam); + SetEntProp(iClient, Prop_Send, "m_lifeState", iLifeState); + + int iWearable; + int iWearablesCount = GetPlayerWearablesCount(iClient); + Address pData = DereferencePointer(GetEntityAddress(iClient) + MyWearables); + + for (int iIndex = 0; iIndex < iWearablesCount; iIndex++) + { + iWearable = LoadEntityHandleFromAddress(pData + view_as
(0x04 * iIndex)); + + SetEntProp(iWearable, Prop_Send, "m_nSkin", (iTeam == view_as(TFTeam_Blue)) ? 1 : 0); + SetEntProp(iWearable, Prop_Send, "m_iTeamNum", iTeam); + } +} + +/* + https://github.com/nosoop/SM-TFUtils/blob/master/scripting/tf2utils.sp + https://github.com/nosoop/stocksoup/blob/master/memory.inc +*/ + +stock int LoadEntityHandleFromAddress(Address pAddress) +{ + return EntRefToEntIndex(LoadFromAddress(pAddress, NumberType_Int32) | (1 << 31)); +} + +stock Address DereferencePointer(Address pAddress) +{ + // maybe someday we'll do 64-bit addresses + return view_as
(LoadFromAddress(pAddress, NumberType_Int32)); +} + +int GetPlayerWearablesCount(int iClient) +{ + return GetEntData(iClient, view_as(MyWearables) + 0x0C); +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_no_block.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_no_block.sp new file mode 100644 index 00000000..f59c1076 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_no_block.sp @@ -0,0 +1,68 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include + +#include + +#define PLUGIN_NAME "[TFDB] No block" +#define PLUGIN_AUTHOR "x07x08" +#define PLUGIN_DESCRIPTION "Removes collision between enemies." +#define PLUGIN_VERSION "1.0.3" +#define PLUGIN_URL "https://github.com/x07x08/TF2-Dodgeball-Modified" + +#define COLLISION_GROUP_PUSHAWAY 17 + +bool Loaded; + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_URL +}; + +public void OnPluginStart() +{ + if (!TFDB_IsDodgeballEnabled()) return; + + TFDB_OnRocketsConfigExecuted("general.cfg"); + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || !IsPlayerAlive(iClient)) continue; + + SetEntProp(iClient, Prop_Data, "m_CollisionGroup", COLLISION_GROUP_PUSHAWAY); + } +} + +public void TFDB_OnRocketsConfigExecuted(const char[] strConfigFile) +{ + if (Loaded) return; + + HookEvent("player_spawn", OnPlayerSpawn); + + Loaded = true; +} + +public void OnMapEnd() +{ + if (!Loaded) return; + + UnhookEvent("player_spawn", OnPlayerSpawn); + + Loaded = false; +} + +public void OnPlayerSpawn(Event hEvent, char[] strEventName, bool bDontBroadcast) +{ + int iClient = GetClientOfUserId(hEvent.GetInt("userid")); + + if (GetClientTeam(iClient) <= 1) return; + + // SetEntityCollisionGroup makes the server crash after a while. No idea why. + SetEntProp(iClient, Prop_Data, "m_CollisionGroup", COLLISION_GROUP_PUSHAWAY); +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_print.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_print.sp new file mode 100644 index 00000000..a3cfda59 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_print.sp @@ -0,0 +1,262 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include + +#define PLUGIN_NAME "[TFDB] Print & replace client indexes" +#define PLUGIN_AUTHOR "x07x08 & Silorak" +#define PLUGIN_DESCRIPTION "Does what it says" +#define PLUGIN_VERSION "1.1.3" +#define PLUGIN_URL "https://github.com/x07x08/TF2-Dodgeball-Modified" + +char CmdBuffer[255]; +char ExplodeBuffer[32][255]; + +Regex BracketsPattern; + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_URL +}; + +public void OnPluginStart() +{ + LoadTranslations("tfdb.phrases.txt"); + + BracketsPattern = new Regex("(?<=\\[)(.*?)(?=\\])"); + + RegAdminCmd("tf_dodgeball_print", CmdPrintMessage, ADMFLAG_CHAT, "Prints a message to chat and replaces client indexes inside a pair of '##'"); + RegAdminCmd("tf_dodgeball_print_c", CmdPrintMessageClient, ADMFLAG_CHAT, "Prints a message to a client and replaces client indexes inside a pair of '##'"); + RegAdminCmd("tf_dodgeball_phrase", CmdPrintPhrase, ADMFLAG_CHAT, "Prints a translation phrase to chat"); + RegAdminCmd("tf_dodgeball_phrase_c", CmdPrintPhraseClient, ADMFLAG_CHAT, "Prints a translation phrase to a client"); +} + +public Action CmdPrintMessage(int iClient, int iArgs) +{ + if (!(iArgs >= 1)) + { + ReplyToCommand(iClient, "Usage : tf_dodgeball_print "); + + return Plugin_Handled; + } + + GetCmdArgString(CmdBuffer, sizeof(CmdBuffer)); + TrimString(CmdBuffer); + + int iNumStrings = ExplodeString(CmdBuffer, "##", ExplodeBuffer, sizeof(ExplodeBuffer), sizeof(ExplodeBuffer[])); + int iIndex; + + for (int iPos = 0; iPos < iNumStrings; iPos++) + { + if (!ExplodeBuffer[iPos][0]) continue; + + if ((StringToIntEx(ExplodeBuffer[iPos], iIndex) == strlen(ExplodeBuffer[iPos])) && + ((iIndex >= 1) && (iIndex <= MaxClients) && IsClientInGame(iIndex))) + { + FormatEx(ExplodeBuffer[iPos], sizeof(ExplodeBuffer[]), "%N", iIndex); + } + } + + ImplodeStrings(ExplodeBuffer, iNumStrings, "", CmdBuffer, sizeof(CmdBuffer)); + + CPrintToChatAll(CmdBuffer); + + return Plugin_Handled; +} + +public Action CmdPrintMessageClient(int iClient, int iArgs) +{ + if (!(iArgs >= 2)) + { + ReplyToCommand(iClient, "Usage : tf_dodgeball_print_c "); + + return Plugin_Handled; + } + + char strBuffer[8]; + + GetCmdArgString(CmdBuffer, sizeof(CmdBuffer)); + + int iLength = BreakString(CmdBuffer, strBuffer, sizeof(strBuffer)); + int iTarget = StringToInt(strBuffer); + + TrimString(CmdBuffer[iLength]); + + int iNumStrings = ExplodeString(CmdBuffer[iLength], "##", ExplodeBuffer, sizeof(ExplodeBuffer), sizeof(ExplodeBuffer[])); + int iIndex; + + for (int iPos = 0; iPos < iNumStrings; iPos++) + { + if (!ExplodeBuffer[iPos][0]) continue; + + if ((StringToIntEx(ExplodeBuffer[iPos], iIndex) == strlen(ExplodeBuffer[iPos])) && + ((iIndex >= 1) && (iIndex <= MaxClients) && IsClientInGame(iIndex))) + { + FormatEx(ExplodeBuffer[iPos], sizeof(ExplodeBuffer[]), "%N", iIndex); + } + } + + ImplodeStrings(ExplodeBuffer, iNumStrings, "", CmdBuffer[iLength], sizeof(CmdBuffer)); + + if ((iTarget >= 1) && (iTarget <= MaxClients) && IsClientInGame(iTarget)) + { + CPrintToChat(iTarget, CmdBuffer[iLength]); + } + + return Plugin_Handled; +} + +public Action CmdPrintPhrase(int iClient, int iArgs) +{ + char strPhrase[48]; + + GetCmdArgString(CmdBuffer, sizeof(CmdBuffer)); TrimString(CmdBuffer); + + int iMatches = BracketsPattern.MatchAll(CmdBuffer); + + if (!(iMatches >= 1)) + { + ReplyToCommand(iClient, "Usage : tf_dodgeball_phrase (phrase and args must be surrounded by []) (phrase arguments must be separated by a comma [,])"); + + return Plugin_Handled; + } + + any aArgs[32]; + + BracketsPattern.GetSubString(0, strPhrase, sizeof(strPhrase), 0); TrimString(strPhrase); + + if (iMatches == 2) + { + BracketsPattern.GetSubString(0, CmdBuffer, sizeof(CmdBuffer), 1); + + int iStrings = ExplodeString(CmdBuffer, ",", ExplodeBuffer, sizeof(ExplodeBuffer), sizeof(ExplodeBuffer[])); + + for (int iIndex = 0; iIndex < iStrings; iIndex++) + { + TrimString(ExplodeBuffer[iIndex]); + + if ((StringToIntEx(ExplodeBuffer[iIndex], aArgs[iIndex]) == strlen(ExplodeBuffer[iIndex])) || + (StringToFloatEx(ExplodeBuffer[iIndex], aArgs[iIndex]) == strlen(ExplodeBuffer[iIndex]))) + { + ExplodeBuffer[iIndex] = "\0"; + } + } + } + + PrintPhrase(strPhrase, ExplodeBuffer, aArgs, true); + + return Plugin_Handled; +} + +public Action CmdPrintPhraseClient(int iClient, int iArgs) +{ + char strPhrase[48], strTarget[8]; + + GetCmdArgString(CmdBuffer, sizeof(CmdBuffer)); TrimString(CmdBuffer); + + int iMatches = BracketsPattern.MatchAll(CmdBuffer); + + if (!(iMatches >= 2)) + { + ReplyToCommand(iClient, "Usage : tf_dodgeball_phrase_c (client, phrase and args must be surrounded by []) (phrase arguments must be separated by a comma [,])"); + + return Plugin_Handled; + } + + any aArgs[32]; + + BracketsPattern.GetSubString(0, strTarget, sizeof(strTarget), 0); TrimString(strTarget); + BracketsPattern.GetSubString(0, strPhrase, sizeof(strPhrase), 1); TrimString(strPhrase); + + int iTarget = StringToInt(strTarget); + + if (iMatches == 3) + { + BracketsPattern.GetSubString(0, CmdBuffer, sizeof(CmdBuffer), 2); + + int iStrings = ExplodeString(CmdBuffer, ",", ExplodeBuffer, sizeof(ExplodeBuffer), sizeof(ExplodeBuffer[])); + + for (int iIndex = 0; iIndex < iStrings; iIndex++) + { + TrimString(ExplodeBuffer[iIndex]); + + if ((StringToIntEx(ExplodeBuffer[iIndex], aArgs[iIndex]) == strlen(ExplodeBuffer[iIndex])) || + (StringToFloatEx(ExplodeBuffer[iIndex], aArgs[iIndex]) == strlen(ExplodeBuffer[iIndex]))) + { + ExplodeBuffer[iIndex] = "\0"; + } + } + } + + if ((iTarget >= 1) && (iTarget <= MaxClients) && IsClientInGame(iTarget)) + { + PrintPhrase(strPhrase, ExplodeBuffer, aArgs, false, iTarget); + } + + return Plugin_Handled; +} + +void PrintPhrase(const char[] strPhrase, const char strArgs[32][255], const any aArgs[32], bool bAll, int iClient = -1) +{ + // Use SetGlobalTransTarget + Format with %T to properly handle + // dynamic translation arguments without heap overflow. + // Maximum phrase args in tfdb.phrases.txt is 5, so 8 slots is plenty. + + if (bAll) + { + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) continue; + + char strBuffer[512]; + SetGlobalTransTarget(i); + FormatEx(strBuffer, sizeof(strBuffer), "%T", strPhrase, i, + HBC(strArgs, aArgs, 0), HBC(strArgs, aArgs, 1), + HBC(strArgs, aArgs, 2), HBC(strArgs, aArgs, 3), + HBC(strArgs, aArgs, 4), HBC(strArgs, aArgs, 5), + HBC(strArgs, aArgs, 6), HBC(strArgs, aArgs, 7)); + + CPrintToChat(i, strBuffer); + } + } + else + { + char strBuffer[512]; + SetGlobalTransTarget(iClient); + FormatEx(strBuffer, sizeof(strBuffer), "%T", strPhrase, iClient, + HBC(strArgs, aArgs, 0), HBC(strArgs, aArgs, 1), + HBC(strArgs, aArgs, 2), HBC(strArgs, aArgs, 3), + HBC(strArgs, aArgs, 4), HBC(strArgs, aArgs, 5), + HBC(strArgs, aArgs, 6), HBC(strArgs, aArgs, 7)); + + CPrintToChat(iClient, strBuffer); + } +} + +// Returns either the numeric value or the string as any[]. +// SM 1.12 requires any[] return type — cannot coerce char[] to any scalar. +// With only 8 calls instead of 29, this fits comfortably in default heap. +any[] HBC(const char[][] strArgs, const any[] aArgs, int iIndex) +{ + static any aResult[256]; + + if (!strArgs[iIndex][0]) + { + aResult[0] = aArgs[iIndex]; + return aResult; + } + + int i; + for (i = 0; i < 255 && strArgs[iIndex][i]; i++) + { + aResult[i] = view_as(strArgs[iIndex][i]); + } + aResult[i] = 0; + return aResult; +} + diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_speedhud.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_speedhud.sp new file mode 100644 index 00000000..28a7b79a --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_speedhud.sp @@ -0,0 +1,343 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include +#include // Include the Dodgeball plugin's natives +#include // Include for cookie functions +#include // Include for colored chat and translations + +#define PLUGIN_VERSION "2.0" + +public Plugin myinfo = +{ + name = "[TF2] Dodgeball Speed HUD", + author = "Silorak", + description = "Displays the speed of active rockets to all players.", + version = PLUGIN_VERSION, + url = "https://github.com/Silorak/TF2-Dodgeball-Modified" +}; + +// ==================================================================================================== +// Global Variables +// ==================================================================================================== + +ConVar CvarHudEnabled; +Handle DisplayTimer; +Handle CookieHudPref; // Handle for the client's HUD preference cookie. + +// Tracks if the HUD is currently being displayed for a client. +bool IsHudVisible[MAXPLAYERS + 1]; +// Tracks a client's personal preference for seeing the HUD. +bool HudEnabledForClient[MAXPLAYERS + 1]; + +// ==================================================================================================== +// Plugin Lifecycle +// ==================================================================================================== + +public void OnPluginStart() +{ + CreateConVar("tfdb_speedhud_version", PLUGIN_VERSION, "Dodgeball Speed HUD Version", FCVAR_NOTIFY|FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_DONTRECORD); + CvarHudEnabled = CreateConVar("tfdb_speedhud_enabled", "1", "Enable the rocket speed HUD for all players.", _, true, 0.0, true, 1.0); + + // Register the command for players to toggle the HUD. + RegConsoleCmd("sm_speedhud", Command_ToggleHud, "Toggles the rocket speed HUD display."); + RegConsoleCmd("sm_shud", Command_ToggleHud, "Toggles the rocket speed HUD display."); // Alias + + // Load the translations from the main Dodgeball plugin. + LoadTranslations("tfdb.phrases.txt"); + + // Register the cookie. The second argument is the default value. + CookieHudPref = RegClientCookie("tfdb_speedhud_pref", "Toggle for the Dodgeball Speed HUD", CookieAccess_Public); + + // Hook the ConVar change to enable/disable the timer on the fly. + CvarHudEnabled.AddChangeHook(OnConVarChanged); + + // Hook client connection to load their preference from cookies. + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + OnClientPostAdminCheck(i); + } + } + + // Start the timer if the plugin is loaded while the cvar is enabled. + if (CvarHudEnabled.BoolValue) + { + StartDisplayTimer(); + } +} + +public void OnPluginEnd() +{ + // Clean up the timer when the plugin unloads. + StopDisplayTimer(); +} + +public void OnMapStart() +{ + // Check if the Dodgeball plugin is running and if the HUD is enabled. + if (LibraryExists("tfdb") && CvarHudEnabled.BoolValue) + { + StartDisplayTimer(); + } +} + +public void OnMapEnd() +{ + // Clean up the timer at the end of the map. + StopDisplayTimer(); +} + +public void OnClientPostAdminCheck(int client) +{ + // Load the client's preference when they fully connect. + char sCookie[8]; + GetClientCookie(client, CookieHudPref, sCookie, sizeof(sCookie)); + + // Default to ON if the cookie is not set or is set to "1". + if (sCookie[0] == '0') + { + HudEnabledForClient[client] = false; + } + else + { + HudEnabledForClient[client] = true; + } +} + +// ==================================================================================================== +// ConVar & Command Management +// ==================================================================================================== + +public Action Command_ToggleHud(int client, int args) +{ + // This command is for players only. + if (client == 0) + { + PrintToServer("This command can only be used by players."); + return Plugin_Handled; + } + + // Flip the player's preference. + HudEnabledForClient[client] = !HudEnabledForClient[client]; + + if (HudEnabledForClient[client]) + { + // Set cookie to "1" for ON. + SetClientCookie(client, CookieHudPref, "1"); + CPrintToChat(client, "%t", "Hud_Enabled"); + } + else + { + // Set cookie to "0" for OFF. + SetClientCookie(client, CookieHudPref, "0"); + CPrintToChat(client, "%t", "Hud_Disabled"); + } + + return Plugin_Handled; +} + +public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (convar == CvarHudEnabled) + { + if (StringToInt(newValue) == 1) + { + StartDisplayTimer(); + } + else + { + StopDisplayTimer(); + } + } +} + +/** + * Starts the main timer for displaying the HUD. + */ +void StartDisplayTimer() +{ + // Don't create a new timer if one already exists. + if (DisplayTimer != null) + { + return; + } + // Create a repeating timer that calls DisplayHud every 0.1 seconds. + DisplayTimer = CreateTimer(0.1, DisplayHud, _, TIMER_REPEAT); +} + +/** + * Stops the main display timer and clears the HUD for all players. + */ +void StopDisplayTimer() +{ + if (DisplayTimer != null) + { + KillTimer(DisplayTimer); + DisplayTimer = null; + } + + // Clear the HUD for any player who might still have it open. + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && IsHudVisible[i]) + { + // Set an empty message to clear the HUD. + SetHudTextParams(0.0, 0.0, 0.1, 255, 255, 255, 0, 0, 0.0, 0.0, 0.0); + ShowHudText(i, 4, " "); + IsHudVisible[i] = false; + } + } +} + +// ==================================================================================================== +// Core Logic +// ==================================================================================================== + +/** + * Timer callback that runs continuously to update the HUD for all players. + */ +public Action DisplayHud(Handle timer) +{ + // Only run if the main Dodgeball plugin is enabled and the cvar is on. + if (!CvarHudEnabled.BoolValue || !TFDB_IsDodgeballEnabled()) + { + // Ensure the HUD is cleared if the plugin is disabled globally. + StopDisplayTimer(); + return Plugin_Continue; + } + + // --- Collect and sort active rockets --- + int rocketIndices[MAX_ROCKETS]; + float rocketSpeeds[MAX_ROCKETS]; + int rocketCount = 0; + + for (int i = 0; i < MAX_ROCKETS; i++) + { + if (TFDB_IsValidRocket(i)) + { + rocketIndices[rocketCount] = i; + rocketSpeeds[rocketCount] = TFDB_GetRocketMphSpeed(i); + rocketCount++; + } + } + + // --- Display logic based on rocket count --- + if (rocketCount == 0) + { + // No rockets are active, clear the HUD for anyone who has it visible. + for (int i = 1; i <= MaxClients; i++) + { + if (IsHudVisible[i] && IsClientInGame(i)) + { + SetHudTextParams(0.0, 0.0, 0.1, 255, 255, 255, 0, 0, 0.0, 0.0, 0.0); + ShowHudText(i, 4, " "); + IsHudVisible[i] = false; + } + } + } + else if (rocketCount == 1) + { + // Single rocket: Display in the center. + int rocketIndex = rocketIndices[0]; + float mphSpeed = rocketSpeeds[0]; + float huSpeed = TFDB_GetRocketSpeed(rocketIndex); + int deflections = TFDB_GetRocketDeflections(rocketIndex); + int rocketClass = TFDB_GetRocketClass(rocketIndex); + char className[64]; + TFDB_GetRocketClassLongName(rocketClass, className, sizeof(className)); + + char hudMessage[256]; + // Format the string first to avoid issues with ShowHudText. + FormatEx(hudMessage, sizeof(hudMessage), "%t", "Hud_Speedometer", mphSpeed, huSpeed, deflections, rocketIndex + 1, className); + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + if (HudEnabledForClient[i]) + { + SetHudTextParams(-1.0, 0.85, 0.15, 100, 255, 100, 255, 0, 0.0, 0.0, 0.15); + ShowHudText(i, 4, hudMessage); + IsHudVisible[i] = true; + } + else if (IsHudVisible[i]) + { + SetHudTextParams(0.0, 0.0, 0.1, 255, 255, 255, 0, 0, 0.0, 0.0, 0.0); + ShowHudText(i, 4, " "); + IsHudVisible[i] = false; + } + } + } + } + else // Multiple rockets + { + // Sort rockets by speed (descending) using a simple bubble sort. + for (int i = 0; i < rocketCount - 1; i++) + { + for (int j = 0; j < rocketCount - i - 1; j++) + { + if (rocketSpeeds[j] < rocketSpeeds[j + 1]) + { + // Swap speeds + float tempSpeed = rocketSpeeds[j]; + rocketSpeeds[j] = rocketSpeeds[j + 1]; + rocketSpeeds[j + 1] = tempSpeed; + // Swap indices + int tempIndex = rocketIndices[j]; + rocketIndices[j] = rocketIndices[j + 1]; + rocketIndices[j + 1] = tempIndex; + } + } + } + + // Build the multi-line HUD message. + char hudMessage[1024]; + int maxRocketsToShow = 5; // Limit to prevent screen clutter. + int numToShow = rocketCount < maxRocketsToShow ? rocketCount : maxRocketsToShow; + + for (int i = 0; i < numToShow; i++) + { + int rocketIndex = rocketIndices[i]; + float mphSpeed = rocketSpeeds[i]; + float huSpeed = TFDB_GetRocketSpeed(rocketIndex); + int deflections = TFDB_GetRocketDeflections(rocketIndex); + int rocketClass = TFDB_GetRocketClass(rocketIndex); + char className[64]; + TFDB_GetRocketClassLongName(rocketClass, className, sizeof(className)); + + char line[256]; + // Pass the floats directly, the translation file will format them. + Format(line, sizeof(line), "%t\n", "Hud_SpeedometerEx", mphSpeed, huSpeed, deflections, i + 1, className); + + if (i == 0) + strcopy(hudMessage, sizeof(hudMessage), line); + else + Format(hudMessage, sizeof(hudMessage), "%s%s", hudMessage, line); + } + + // Display the list on the left side for all players. + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + if (HudEnabledForClient[i]) + { + SetHudTextParams(0.05, 0.4, 0.15, 100, 255, 100, 255, 0, 0.0, 0.0, 0.15); + ShowHudText(i, 4, hudMessage); + IsHudVisible[i] = true; + } + else if (IsHudVisible[i]) + { + SetHudTextParams(0.0, 0.0, 0.1, 255, 255, 255, 0, 0, 0.0, 0.0, 0.0); + ShowHudText(i, 4, " "); + IsHudVisible[i] = false; + } + } + } + } + return Plugin_Continue; +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp new file mode 100644 index 00000000..62db3584 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp @@ -0,0 +1,713 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include + +#include + +#define PLUGIN_NAME "[TFDB] Votes" +#define PLUGIN_AUTHOR "x07x08" +#define PLUGIN_DESCRIPTION "Various rocket votes." +#define PLUGIN_VERSION "1.1.0" +#define PLUGIN_URL "https://github.com/Silorak/TF2-Dodgeball-Modified" + +int g_iSpawnersCount; + +ConVar CvarVoteBounceDuration; +ConVar CvarVoteClassDuration; +ConVar CvarVoteCountDuration; +ConVar CvarVoteBounceTimeout; +ConVar CvarVoteClassTimeout; +ConVar CvarVoteCountTimeout; +ConVar CvarVotePresetDuration; +ConVar CvarVotePresetTimeout; + +bool VoteBounceAllowed; +bool VoteClassAllowed; +bool VoteCountAllowed; +bool VotePresetAllowed; + +float LastVoteBounceTime; +float LastVoteClassTime; +float LastVoteCountTime; +float LastVotePresetTime; + +bool BounceEnabled; +int MainRocketClass = -1; +int RocketsCount = -1; + +int SavedMaxRockets[MAX_SPAWNER_CLASSES]; + +bool Loaded; + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_URL +}; + +public void OnPluginStart() +{ + LoadTranslations("tfdb.phrases.txt"); + + CvarVoteBounceDuration = CreateConVar("tf_dodgeball_votes_bounce_duration", "20", _, _, true, 0.0); + CvarVoteClassDuration = CreateConVar("tf_dodgeball_votes_class_duration", "20", _, _, true, 0.0); + CvarVoteCountDuration = CreateConVar("tf_dodgeball_votes_count_duration", "20", _, _, true, 0.0); + CvarVoteBounceTimeout = CreateConVar("tf_dodgeball_votes_bounce_timeout", "150", _, _, true, 0.0); + CvarVoteClassTimeout = CreateConVar("tf_dodgeball_votes_class_timeout", "150", _, _, true, 0.0); + CvarVoteCountTimeout = CreateConVar("tf_dodgeball_votes_count_timeout", "150", _, _, true, 0.0); + CvarVotePresetDuration = CreateConVar("tf_dodgeball_votes_preset_duration", "20", _, _, true, 0.0); + CvarVotePresetTimeout = CreateConVar("tf_dodgeball_votes_preset_timeout", "150", _, _, true, 0.0); + + RegConsoleCmd("sm_vrb", CmdVoteBounce, "Start a rocket bounce vote"); + RegConsoleCmd("sm_vrc", CmdVoteClass, "Start a rocket class vote"); + RegConsoleCmd("sm_vrcount", CmdVoteCount, "Start a rocket count vote"); + RegConsoleCmd("sm_vrp", CmdVotePreset, "Start a preset vote"); + RegConsoleCmd("sm_votebounce", CmdVoteBounce, "Start a rocket bounce vote"); + RegConsoleCmd("sm_voteclass", CmdVoteClass, "Start a rocket class vote"); + RegConsoleCmd("sm_votecount", CmdVoteCount, "Start a rocket count vote"); + RegConsoleCmd("sm_votepreset", CmdVotePreset, "Start a preset vote"); + RegConsoleCmd("sm_voterocketbounce", CmdVoteBounce, "Start a rocket bounce vote"); + RegConsoleCmd("sm_voterocketclass", CmdVoteClass, "Start a rocket class vote"); + RegConsoleCmd("sm_voterocketcount", CmdVoteCount, "Start a rocket count vote"); + RegConsoleCmd("sm_voterocketpreset", CmdVotePreset, "Start a preset vote"); + + if (!TFDB_IsDodgeballEnabled()) return; + + char strMapName[64]; GetCurrentMap(strMapName, sizeof(strMapName)); + GetMapDisplayName(strMapName, strMapName, sizeof(strMapName)); + char strMapFile[PLATFORM_MAX_PATH]; FormatEx(strMapFile, sizeof(strMapFile), "%s.cfg", strMapName); + + TFDB_OnRocketsConfigExecuted("general.cfg"); + TFDB_OnRocketsConfigExecuted(strMapFile); +} + +public void OnMapEnd() +{ + if (!Loaded) return; + + VoteBounceAllowed = + VoteClassAllowed = + VoteCountAllowed = + VotePresetAllowed = false; + + LastVoteBounceTime = + LastVoteClassTime = + LastVoteCountTime = + LastVotePresetTime = 0.0; + + BounceEnabled = false; + MainRocketClass = -1; + RocketsCount = -1; + + Loaded = false; + + g_iSpawnersCount = 0; +} + +public void TFDB_OnRocketsConfigExecuted(const char[] strConfigFile) +{ + if (!Loaded) + { + VoteBounceAllowed = + VoteClassAllowed = + VoteCountAllowed = + VotePresetAllowed = true; + + LastVoteBounceTime = + LastVoteClassTime = + LastVoteCountTime = + LastVotePresetTime = 0.0; + + BounceEnabled = false; + MainRocketClass = -1; + RocketsCount = -1; + + Loaded = true; + } + + if (strcmp(strConfigFile, "general.cfg") == 0) + { + g_iSpawnersCount = 0; + } + + ParseConfigurations(strConfigFile); +} + +public Action CmdVoteBounce(int iClient, int iArgs) +{ + if (iClient == 0) + { + ReplyToCommand(iClient, "Command is in-game only."); + + return Plugin_Handled; + } + + if (!TFDB_IsDodgeballEnabled()) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + if (IsVoteInProgress()) + { + CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); + + return Plugin_Handled; + } + + if (VoteBounceAllowed) + { + VoteBounceAllowed = false; + LastVoteBounceTime = GetGameTime(); + + StartBounceVote(); + CreateTimer(CvarVoteBounceTimeout.FloatValue, VoteBounceTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); + } + else + { + CReplyToCommand(iClient, "%t", "Dodgeball_BounceVote_Cooldown", + RoundToCeil((LastVoteBounceTime + CvarVoteBounceTimeout.FloatValue) - GetGameTime())); + } + + return Plugin_Handled; +} + +void StartBounceVote() +{ + char strMode[16]; + strMode = !BounceEnabled ? "Enable" : "Disable"; + + Menu hMenu = new Menu(VoteMenuHandler); + hMenu.VoteResultCallback = VoteBounceResultHandler; + + hMenu.SetTitle("%s no rocket bounce mode?", strMode); + + hMenu.AddItem("0", "Yes"); + hMenu.AddItem("1", "No"); + + int iTotal; + int[] iClients = new int[MaxClients]; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) + { + continue; + } + + iClients[iTotal++] = iClient; + } + + hMenu.DisplayVote(iClients, iTotal, CvarVoteBounceDuration.IntValue); +} + +public int VoteMenuHandler(Menu hMenu, MenuAction iMenuActions, int iParam1, int iParam2) +{ + switch (iMenuActions) + { + case MenuAction_End : + { + delete hMenu; + } + } + + return 0; +} + +public void VoteBounceResultHandler(Menu hMenu, + int iNumVotes, + int iNumClients, + const int[][] iClientInfo, + int iNumItems, + const int[][] iItemInfo) +{ + int iWinnerIndex = 0; + + if (iNumItems > 1 && + (iItemInfo[0][VOTEINFO_ITEM_VOTES] == iItemInfo[1][VOTEINFO_ITEM_VOTES])) + { + iWinnerIndex = GetRandomInt(0, 1); + } + + char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); + + if (StrEqual(strWinner, "0")) + { + ToggleBounce(); + } + else + { + CPrintToChatAll("%t", "Dodgeball_BounceVote_Failed"); + } +} + +void ToggleBounce() +{ + if (!BounceEnabled) + { + EnableBounce(); + } + else + { + DisableBounce(); + } +} + +void EnableBounce() +{ + BounceEnabled = true; + + for (int iIndex = 0; iIndex < MAX_ROCKETS; iIndex++) + { + if (!TFDB_IsValidRocket(iIndex)) continue; + + TFDB_SetRocketBounces(iIndex, TFDB_GetRocketClassMaxBounces(TFDB_GetRocketClass(iIndex))); + } + + CPrintToChatAll("%t", "Dodgeball_BounceVote_Enabled"); +} + +void DisableBounce() +{ + BounceEnabled = false; + + for (int iIndex = 0; iIndex < MAX_ROCKETS; iIndex++) + { + if (!TFDB_IsValidRocket(iIndex)) continue; + + TFDB_SetRocketBounces(iIndex, 0); + } + + CPrintToChatAll("%t", "Dodgeball_BounceVote_Disabled"); +} + +public Action CmdVoteClass(int iClient, int iArgs) +{ + if (iClient == 0) + { + ReplyToCommand(iClient, "Command is in-game only."); + + return Plugin_Handled; + } + + if (!TFDB_IsDodgeballEnabled()) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + if (IsVoteInProgress()) + { + CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); + + return Plugin_Handled; + } + + if (VoteClassAllowed) + { + VoteClassAllowed = false; + LastVoteClassTime = GetGameTime(); + + StartClassVote(); + CreateTimer(CvarVoteClassTimeout.FloatValue, VoteClassTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); + } + else + { + CReplyToCommand(iClient, "%t", "Dodgeball_ClassVote_Cooldown", + RoundToCeil((LastVoteClassTime + CvarVoteClassTimeout.FloatValue) - GetGameTime())); + } + + return Plugin_Handled; +} + +void StartClassVote() +{ + Menu hMenu = new Menu(VoteMenuHandler); + hMenu.VoteResultCallback = VoteClassResultHandler; + + hMenu.SetTitle("Change main rocket class?"); + + if (MainRocketClass != -1) + { + hMenu.AddItem("-1", "Reset the spawn chances"); + } + + char strClass[8], strRocketClassLongName[32]; + + for (int iClass = 0; iClass < TFDB_GetRocketClassCount(); iClass++) + { + IntToString(iClass, strClass, sizeof(strClass)); + TFDB_GetRocketClassLongName(iClass, strRocketClassLongName, sizeof(strRocketClassLongName)); + + hMenu.AddItem(strClass, strRocketClassLongName, ITEMDRAW_DEFAULT); + } + + int iTotal; + int[] iClients = new int[MaxClients]; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) + { + continue; + } + + iClients[iTotal++] = iClient; + } + + hMenu.DisplayVote(iClients, iTotal, CvarVoteClassDuration.IntValue); +} + +public void VoteClassResultHandler(Menu hMenu, + int iNumVotes, + int iNumClients, + const int[][] iClientInfo, + int iNumItems, + const int[][] iItemInfo) +{ + int iWinnerIndex = 0; + int iClassCount = TFDB_GetRocketClassCount(); + + if (MainRocketClass != -1) iClassCount++; + + bool bEqual = AreVotesEqual(iItemInfo, iClassCount); + + if (bEqual) iWinnerIndex = GetRandomInt(0, (iClassCount - 1)); + + char strWinner[8], strClassLongName[32]; + + hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner), _, strClassLongName, sizeof(strClassLongName)); + + MainRocketClass = StringToInt(strWinner); + + if (MainRocketClass == -1) + { + CPrintToChatAll("%t", "Dodgeball_ClassVote_Reset"); + } + else + { + CPrintToChatAll("%t", "Dodgeball_ClassVote_Changed", strClassLongName); + } + + TFDB_DestroyRockets(); +} + +public Action CmdVoteCount(int iClient, int iArgs) +{ + if (iClient == 0) + { + ReplyToCommand(iClient, "Command is in-game only."); + + return Plugin_Handled; + } + + if (!TFDB_IsDodgeballEnabled()) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + if (IsVoteInProgress()) + { + CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); + + return Plugin_Handled; + } + + if (VoteCountAllowed) + { + VoteCountAllowed = false; + LastVoteCountTime = GetGameTime(); + + StartCountVote(); + CreateTimer(CvarVoteCountTimeout.FloatValue, VoteCountTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); + } + else + { + CReplyToCommand(iClient, "%t", "Dodgeball_CountVote_Cooldown", + RoundToCeil((LastVoteCountTime + CvarVoteCountTimeout.FloatValue) - GetGameTime())); + } + + return Plugin_Handled; +} + +void StartCountVote() +{ + Menu hMenu = new Menu(VoteMenuHandler); + hMenu.VoteResultCallback = VoteCountResultHandler; + + hMenu.SetTitle("Change rockets count?"); + + if (RocketsCount != -1) + { + hMenu.AddItem("-1", "Reset rockets count"); + } + + hMenu.AddItem("0", "One rocket"); + hMenu.AddItem("1", "Two rockets"); + hMenu.AddItem("2", "Three rockets"); + hMenu.AddItem("3", "Four rockets"); + hMenu.AddItem("4", "Five rockets"); + + int iTotal; + int[] iClients = new int[MaxClients]; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) + { + continue; + } + + iClients[iTotal++] = iClient; + } + + hMenu.DisplayVote(iClients, iTotal, CvarVoteCountDuration.IntValue); +} + +public void VoteCountResultHandler(Menu hMenu, + int iNumVotes, + int iNumClients, + const int[][] iClientInfo, + int iNumItems, + const int[][] iItemInfo) +{ + int iWinnerIndex = 0; + int iVotesCount = 5; + + if (RocketsCount != -1) iVotesCount++; + + bool bEqual = AreVotesEqual(iItemInfo, iVotesCount); + + if (bEqual) iWinnerIndex = GetRandomInt(0, (iVotesCount - 1)); + + char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); + + RocketsCount = StringToInt(strWinner); + + for (int iIndex = 0; iIndex < TFDB_GetSpawnersCount(); iIndex++) + { + TFDB_SetSpawnersMaxRockets(iIndex, RocketsCount == -1 ? SavedMaxRockets[iIndex] : (RocketsCount + 1)); + } + + if (RocketsCount == -1) + { + CPrintToChatAll("%t", "Dodgeball_CountVote_Reset"); + } + else + { + CPrintToChatAll("%t", "Dodgeball_CountVote_Changed", (RocketsCount + 1)); + } +} + +public Action CmdVotePreset(int iClient, int iArgs) +{ + if (iClient == 0) + { + ReplyToCommand(iClient, "Command is in-game only."); + + return Plugin_Handled; + } + + if (!TFDB_IsDodgeballEnabled()) + { + CReplyToCommand(iClient, "%t", "Command_Disabled"); + + return Plugin_Handled; + } + + if (IsVoteInProgress()) + { + CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); + + return Plugin_Handled; + } + + if (TFDB_GetPresetCount() == 0) + { + CReplyToCommand(iClient, "%t", "Dodgeball_PresetVote_NoPresets"); + + return Plugin_Handled; + } + + if (VotePresetAllowed) + { + VotePresetAllowed = false; + LastVotePresetTime = GetGameTime(); + + StartPresetVote(); + CreateTimer(CvarVotePresetTimeout.FloatValue, VotePresetTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); + } + else + { + CReplyToCommand(iClient, "%t", "Dodgeball_PresetVote_Cooldown", + RoundToCeil((LastVotePresetTime + CvarVotePresetTimeout.FloatValue) - GetGameTime())); + } + + return Plugin_Handled; +} + +void StartPresetVote() +{ + Menu hMenu = new Menu(VoteMenuHandler); + hMenu.VoteResultCallback = VotePresetResultHandler; + + hMenu.SetTitle("Select gameplay preset:"); + + int iPresetCount = TFDB_GetPresetCount(); + for (int i = 0; i < iPresetCount; i++) + { + char strIndex[8], strName[64]; + IntToString(i, strIndex, sizeof(strIndex)); + TFDB_GetPresetName(i, strName, sizeof(strName)); + hMenu.AddItem(strIndex, strName); + } + + int iTotal; + int[] iClients = new int[MaxClients]; + + for (int iClient = 1; iClient <= MaxClients; iClient++) + { + if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) + { + continue; + } + + iClients[iTotal++] = iClient; + } + + hMenu.DisplayVote(iClients, iTotal, CvarVotePresetDuration.IntValue); +} + +public void VotePresetResultHandler(Menu hMenu, + int iNumVotes, + int iNumClients, + const int[][] iClientInfo, + int iNumItems, + const int[][] iItemInfo) +{ + int iWinnerIndex = 0; + + bool bEqual = AreVotesEqual(iItemInfo, iNumItems); + + if (bEqual) iWinnerIndex = GetRandomInt(0, (iNumItems - 1)); + + char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); + + int iPreset = StringToInt(strWinner); + + if (TFDB_ApplyPreset(iPreset)) + { + char strName[64]; + TFDB_GetPresetName(iPreset, strName, sizeof(strName)); + CPrintToChatAll("%t", "Dodgeball_PresetVote_Applied", strName); + } +} + +public Action VoteBounceTimeoutCallback(Handle hTimer) +{ + VoteBounceAllowed = true; + + return Plugin_Continue; +} + +public Action VoteClassTimeoutCallback(Handle hTimer) +{ + VoteClassAllowed = true; + + return Plugin_Continue; +} + +public Action VoteCountTimeoutCallback(Handle hTimer) +{ + VoteCountAllowed = true; + + return Plugin_Continue; +} + +public Action VotePresetTimeoutCallback(Handle hTimer) +{ + VotePresetAllowed = true; + + return Plugin_Continue; +} + +public Action TFDB_OnRocketCreatedPre(int iIndex, int &iClass, RocketFlags &iFlags) +{ + if (MainRocketClass == -1) return Plugin_Continue; + + iClass = MainRocketClass; + iFlags = TFDB_GetRocketClassFlags(MainRocketClass); + + return Plugin_Changed; +} + +public void TFDB_OnRocketCreated(int iIndex) +{ + if (!BounceEnabled) return; + + TFDB_SetRocketBounces(iIndex, TFDB_GetRocketClassMaxBounces(TFDB_GetRocketClass(iIndex))); +} + +void ParseConfigurations(const char[] strConfigFile) +{ + char strPath[PLATFORM_MAX_PATH]; + char strFileName[PLATFORM_MAX_PATH]; + FormatEx(strFileName, sizeof(strFileName), "configs/dodgeball/%s", strConfigFile); + BuildPath(Path_SM, strPath, sizeof(strPath), strFileName); + + if (!FileExists(strPath, true)) return; + + KeyValues kvConfig = new KeyValues("TF2_Dodgeball"); + + if (kvConfig.ImportFromFile(strPath) == false) SetFailState("Error while parsing the configuration file."); + + kvConfig.GotoFirstSubKey(); + + do + { + char strSection[64]; kvConfig.GetSectionName(strSection, sizeof(strSection)); + + if (StrEqual(strSection, "spawners")) ParseSpawners(kvConfig); + } + while (kvConfig.GotoNextKey()); + + delete kvConfig; +} + +void ParseSpawners(KeyValues kvConfig) +{ + kvConfig.GotoFirstSubKey(); + + do + { + int iIndex = g_iSpawnersCount; + + SavedMaxRockets[iIndex] = kvConfig.GetNum("max rockets", 1); + + g_iSpawnersCount++; + } + while (kvConfig.GotoNextKey()); + + kvConfig.GoBack(); +} + +bool AreVotesEqual(const int[][] iVoteItems, int iSize) +{ + int iFirst = iVoteItems[0][VOTEINFO_ITEM_VOTES]; + + for (int iIndex = 1; iIndex < iSize; iIndex++) + { + if (iVoteItems[iIndex][VOTEINFO_ITEM_VOTES] != iFirst) return false; + } + + return true; +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/thirdperson.sp b/roles/sourcemod/files/addons/sourcemod/scripting/thirdperson.sp new file mode 100644 index 00000000..2ba9f5dc --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/thirdperson.sp @@ -0,0 +1,74 @@ +#pragma semicolon 1 +#pragma tabsize 4 +#pragma newdecls required + +#include +#include + +#define PLUGIN_VERSION "2.2.0" + +bool g_bThirdPersonEnabled[MAXPLAYERS+1] = { false, ... }; + +public Plugin myinfo = +{ + name = "[TF2] Thirdperson", + author = "DarthNinja, Leigh MacDonald", + description = "Allows players to use thirdperson without having to enable client sv_cheats", + version = PLUGIN_VERSION, + url = "https://github.com/leighmacdonald/uncletopia" +}; + +public void OnPluginStart() +{ + CreateConVar("thirdperson_version", PLUGIN_VERSION, "Plugin Version", FCVAR_PLUGIN|FCVAR_NOTIFY); + RegAdminCmd("sm_thirdperson", EnableThirdperson, 0, "Usage: sm_thirdperson"); + RegAdminCmd("tp", EnableThirdperson, 0, "Usage: sm_thirdperson"); + RegAdminCmd("sm_firstperson", DisableThirdperson, 0, "Usage: sm_firstperson"); + RegAdminCmd("fp", DisableThirdperson, 0, "Usage: sm_firstperson"); + HookEvent("player_spawn", OnPlayerSpawned); + HookEvent("player_class", OnPlayerSpawned); +} + +public Action OnPlayerSpawned(Handle event, char[] name, bool dontBroadcast) { + int userid = GetEventInt(event, "userid"); + if (g_bThirdPersonEnabled[GetClientOfUserId(userid)]) { + CreateTimer(0.2, SetViewOnSpawn, userid); + } + + return Plugin_Continue; +} + +public Action SetViewOnSpawn(Handle timer, int userid) { + int client = GetClientOfUserId(userid); + if (client != 0) { + SetVariantInt(1); + AcceptEntityInput(client, "SetForcedTauntCam"); + } + + return Plugin_Continue; +} + +public Action EnableThirdperson(int client, int args) { + if (!IsPlayerAlive(client)) { + PrintToChat(client, "[SM] Thirdperson view will be enabled when you spawn."); + } + SetVariantInt(1); + AcceptEntityInput(client, "SetForcedTauntCam"); + g_bThirdPersonEnabled[client] = true; + + return Plugin_Handled; +} + +public Action DisableThirdperson(int client, int args) { + if (!IsPlayerAlive(client)) { + PrintToChat(client, "[SM] Thirdperson view disabled!"); + } + SetVariantInt(0); + AcceptEntityInput(client, "SetForcedTauntCam"); + g_bThirdPersonEnabled[client] = false; + return Plugin_Handled; +} + +public void OnClientDisconnect(int client) { + g_bThirdPersonEnabled[client] = false; +} diff --git a/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt b/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt new file mode 100644 index 00000000..a17dbad3 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt @@ -0,0 +1,644 @@ +"Phrases" +{ + "Command_Disabled" + { + "en" "[{olive}TFDB{default}] This command is disabled." + } + + "Command_NoAccess" + { + "en" "[{olive}TFDB{default}] You do not have permission to use this command." + } + + "Command_DBExplosion_Usage" + { + "en" "Usage : tf_dodgeball_explosion " + } + + "Command_DBShockwave_Usage" + { + "en" "Usage : tf_dodgeball_shockwave " + } + + "Command_DBRefresh_Done" + { + "#format" "{1:N}" + "en" "[{olive}TFDB{default}] {darkorange}{1}{default} refreshed the {steelblue}dodgeball configs{default}." + } + + "Command_DBHideParticles_Hidden" + { + "en" "[{olive}TFDB{default}] Custom rocket {darkorange}particle{default} trails are now {steelblue}hidden{default}." + } + + "Command_DBHideParticles_Visible" + { + "en" "[{olive}TFDB{default}] Custom rocket {darkorange}particle{default} trails are now {steelblue}visible{default}." + } + + "Command_DBHideSprites_Hidden" + { + "en" "[{olive}TFDB{default}] Custom rocket {darkorange}sprite{default} trails are now {steelblue}hidden{default}." + } + + "Command_DBHideSprites_Visible" + { + "en" "[{olive}TFDB{default}] Custom rocket {darkorange}sprite{default} trails are now {steelblue}visible{default}." + } + + "DBSteal_Warning_Client" + { + "#format" "{1:i},{2:i}" + "en" "[{olive}TFDB{default}] Do not steal rockets. [Warning {darkorange}{1}{default} / {darkorange}{2}{default}]" + } + + "DBSteal_Announce_All" + { + "#format" "{1:N},{2:N}" + "en" "[{olive}TFDB{default}] {darkorange}{1}{default} stole {steelblue}{2}{default}'s rocket!" + } + + "DBSteal_Slay_Client" + { + "en" "[{olive}TFDB{default}] You have been slain for stealing rockets." + } + + "DBSteal_Announce_Slay_All" + { + "#format" "{1:N}" + "en" "[{olive}TFDB{default}] {darkorange}{1}{default} was slain for stealing rockets." + } + + "DBDelay_Announce_All" + { + "#format" "{1:N}" + "en" "[{olive}TFDB{default}] {darkorange}{1}{default} is delaying, the rocket will now speed up." + } + + // Starting from here, the translations are used by other plugins + // Add your own custom phrases here to maintain the order + + "Dodgeball_AirblastPreventionCmd_Enabled" + { + "en" "[{olive}TFDB{default}] {community}Enabled{default} airblast push prevention." + } + + "Dodgeball_AirblastPreventionCmd_Disabled" + { + "en" "[{olive}TFDB{default}] {red}Disabled{default} airblast push prevention." + } + + "Dodgeball_FFAVote_Conflict" + { + "en" "[{olive}TFDB{default}] There is another vote currently in progress." + } + + "Dodgeball_FFAVote_Cooldown" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Voting for {darkmagenta}FFA{default} is in cooldown for {darkorange}{1}{default} seconds." + } + + "Dodgeball_FFAVote_Failed" + { + "en" "[{olive}TFDB{default}] {darkmagenta}FFA{default} vote {red}failed{default}." + } + + "Dodgeball_FFAVote_Enabled" + { + "en" "[{olive}TFDB{default}] {darkmagenta}FFA{default} is now {community}enabled{default}." + } + + "Dodgeball_FFAVote_LateEnabled" + { + "en" "[{olive}TFDB{default}] {darkmagenta}FFA{default} will be {community}enabled{default} once the bot leaves." + } + + "Dodgeball_FFAVote_Disabled" + { + "en" "[{olive}TFDB{default}] {darkmagenta}FFA{default} is now {red}disabled{default}." + } + + "Dodgeball_FFABot_Joined" + { + "en" "[{olive}TFDB{default}] A bot has joined. {darkmagenta}FFA{default} is now {red}disabled{default}." + } + + "Dodgeball_FFABot_Left" + { + "en" "[{olive}TFDB{default}] The bot(s) left. {darkmagenta}FFA{default} is now {community}enabled{default}." + } + + "Dodgeball_Death_Message" + { + "#format" "{1:N},{2:i}" + "en" "[{olive}TFDB{default}] {burlywood}{1}{default} died to a rocket travelling {red}{2}{default} MpH" + } + + "Dodgeball_Disabled" + { + "en" "[{olive}TFDB{default}] Dodgeball is disabled." + } + + "Hud_Enabled" + { + "en" "[{olive}TFDB{default}] Rocket speedometer is now {darkorange}enabled{default}." + } + + "Hud_Disabled" + { + "en" "[{olive}TFDB{default}] Rocket speedometer is now {darkorange}disabled{default}." + } + + // {1} - MpH speed (Miles per Hour) + // {2} - HU speed (Hammer Units) + // {3} - Deflections + // {4} - Index + // {5} - Rocket class long name + + "Hud_Speedometer" + { + "#format" "{1:.0f},{2:.0f},{3:i},{4:i},{5:s}" + "en" "Speed : {1} MpH" + } + + // {1} - MpH speed (Miles per Hour) + // {2} - HU speed (Hammer Units) + // {3} - Deflections + // {4} - Index + // {5} - Rocket class long name + + // This one is for displaying multiple rockets + + "Hud_SpeedometerEx" + { + "#format" "{1:.0f},{2:.0f},{3:i},{4:i},{5:s}" + "en" "{4}. {5} : {1} MpH" + } + + "Menu_NoRockets" + { + "en" "[{olive}TFDB{default}] There are {red}no active{default} rockets." + } + + "Menu_InvalidRocket" + { + "en" "[{olive}TFDB{default}] This rocket is {red}not valid{default} anymore." + } + + "Menu_InvalidClient" + { + "en" "[{olive}TFDB{default}] Client is dead or has disconnected from the server." + } + + "Menu_CannotTarget" + { + "en" "[{olive}TFDB{default}] Can't change target to this client." + } + + "Menu_SameTarget" + { + "en" "[{olive}TFDB{default}] Can't change to the same target." + } + + "Menu_ChangedTarget" + { + "#format" "{1:N}" + "en" "[{olive}TFDB{default}] Changed the target of a rocket to {darkorange}{1}{default}." + } + + "Menu_SameRocketClass" + { + "en" "[{olive}TFDB{default}] Can't change to the same class." + } + + "Menu_ChangedRocketClass" + { + "#format" "{1:s},{2:s}" + "en" "[{olive}TFDB{default}] Changed the class of a rocket from {darkorange}{1}{default} to {steelblue}{2}{default}." + } + + "Menu_Reset" + { + "en" "[{olive}TFDB{default}] Type -1 to reset it." + } + + "Menu_ResetBeepInterval" + { + "en" "[{olive}TFDB{default}] Type -1 to reset it, 0 to disable beeping." + } + + "Menu_ResetSpeedLimit" + { + "en" "[{olive}TFDB{default}] Type -1 to reset it, 0 to disable the speed limit." + } + + "Menu_ResetTurnRateLimit" + { + "en" "[{olive}TFDB{default}] Type -1 to reset it, 0 to disable the turn rate limit." + } + + "Menu_SpriteColor" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new sprite trail color (HEX or RGB) within {darkorange}{1}{default} seconds." + } + + "Menu_SpriteLifetime" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new sprite trail duration within {darkorange}{1}{default} seconds." + } + + "Menu_SpriteStartWidth" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new sprite trail start width within {darkorange}{1}{default} seconds." + } + + "Menu_SpriteEndWidth" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new sprite trail end width within {darkorange}{1}{default} seconds." + } + + "Menu_BeepInterval" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new beep interval within {darkorange}{1}{default} seconds." + } + + "Menu_CritChance" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new critical rocket chance within {darkorange}{1}{default} seconds." + } + + "Menu_Damage" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new damage within {darkorange}{1}{default} seconds." + } + + "Menu_DamageIncrement" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new damage increment within {darkorange}{1}{default} seconds." + } + + "Menu_Speed" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new speed within {darkorange}{1}{default} seconds." + } + + "Menu_SpeedIncrement" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new speed increment within {darkorange}{1}{default} seconds." + } + + "Menu_SpeedLimit" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new speed limit within {darkorange}{1}{default} seconds." + } + + "Menu_TurnRate" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new turn rate within {darkorange}{1}{default} seconds." + } + + "Menu_TurnRateIncrement" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new turn rate increment within {darkorange}{1}{default} seconds." + } + + "Menu_TurnRateLimit" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new turn rate limit within {darkorange}{1}{default} seconds." + } + + "Menu_ElevationRate" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new elevation rate within {darkorange}{1}{default} seconds." + } + + "Menu_ElevationLimit" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new elevation limit within {darkorange}{1}{default} seconds." + } + + "Menu_RocketsModifier" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new fired rockets modifier within {darkorange}{1}{default} seconds." + } + + "Menu_PlayerModifier" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new player count modifier within {darkorange}{1}{default} seconds." + } + + "Menu_ControlDelay" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new control delay within {darkorange}{1}{default} seconds." + } + + "Menu_DragTimeMin" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new drag time start within {darkorange}{1}{default} seconds." + } + + "Menu_DragTimeMax" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new drag time end within {darkorange}{1}{default} seconds." + } + + "Menu_TargetWeight" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new target weight within {darkorange}{1}{default} seconds." + } + + "Menu_MaxBounces" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new maximum bounces within {darkorange}{1}{default} seconds." + } + + "Menu_BounceScale" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new bounce scale within {darkorange}{1}{default} seconds." + } + + "Menu_MaxRockets" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new maximum rockets count within {darkorange}{1}{default} seconds." + } + + "Menu_Interval" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new rocket spawn interval within {darkorange}{1}{default} seconds." + } + + "Menu_ChancesTable" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Type in chat the new rocket class spawn chances within {darkorange}{1}{default} seconds." + } + + // Escaping x07 doesn't work. Parsing it as an argument does the job. + + "Menu_ChangedSpriteColor" + { + "#format" "{1:c},{2:s},{3:s}" + "en" "[{olive}TFDB{default}] Changed rocket class sprite trail color to {1}{2}#{3}." + } + + "Menu_ResetSpriteColor" + { + "#format" "{1:c},{2:s},{3:s}" + "en" "[{olive}TFDB{default}] Reset rocket class sprite trail color to {1}{2}#{3}." + } + + "Menu_ChangedSpriteLifetime" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class sprite trail duration to {darkorange}{1}{default}." + } + + "Menu_ChangedSpriteStartWidth" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class sprite trail start width to {darkorange}{1}{default}." + } + + "Menu_ChangedSpriteEndWidth" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class sprite trail end width to {darkorange}{1}{default}." + } + + "Menu_ChangedBeepInterval" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class beep interval to {darkorange}{1}{default}." + } + + "Menu_ChangedCritChance" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class critical chance to {darkorange}{1}{default}." + } + + "Menu_ChangedDamage" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class damage to {darkorange}{1}{default}." + } + + "Menu_ChangedDamageIncrement" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class damage increment to {darkorange}{1}{default}." + } + + "Menu_ChangedSpeed" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class speed to {darkorange}{1}{default}." + } + + "Menu_ChangedSpeedIncrement" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class speed increment to {darkorange}{1}{default}." + } + + "Menu_ChangedSpeedLimit" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class speed limit to {darkorange}{1}{default}." + } + + "Menu_ChangedTurnRate" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class turn rate to {darkorange}{1}{default}." + } + + "Menu_ChangedTurnRateIncrement" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class turn rate increment to {darkorange}{1}{default}." + } + + "Menu_ChangedTurnRateLimit" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class turn rate limit to {darkorange}{1}{default}." + } + + "Menu_ChangedElevationRate" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class elevation rate to {darkorange}{1}{default}." + } + + "Menu_ChangedElevationLimit" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class elevation limit to {darkorange}{1}{default}." + } + + "Menu_ChangedRocketsModifier" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class fired rockets modifier to {darkorange}{1}{default}." + } + + "Menu_ChangedPlayerModifier" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class player count modifier to {darkorange}{1}{default}." + } + + "Menu_ChangedControlDelay" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class control delay to {darkorange}{1}{default}." + } + + "Menu_ChangedDragTimeMin" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class drag time start to {darkorange}{1}{default}." + } + + "Menu_ChangedDragTimeMax" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class drag time end to {darkorange}{1}{default}." + } + + "Menu_ChangedTargetWeight" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class target weight to {darkorange}{1}{default}." + } + + "Menu_ChangedMaxBounces" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Changed rocket class maximum bounces to {darkorange}{1}{default}." + } + + "Menu_ChangedBounceScale" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed rocket class bounce scale to {darkorange}{1}{default}." + } + + "Menu_ChangedMaxRockets" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Changed spawners maximum rockets to {darkorange}{1}{default}." + } + + "Menu_ChangedInterval" + { + "#format" "{1:.2f}" + "en" "[{olive}TFDB{default}] Changed spawners rocket spawn interval to {darkorange}{1}{default}." + } + + "Menu_ChangedChancesTable" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Changed spawners rocket class chances to {darkorange}{1}{default}." + } + + "Dodgeball_BounceVote_Cooldown" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Voting for bounce toggling is in cooldown for {darkorange}{1}{default} seconds." + } + + "Dodgeball_BounceVote_Failed" + { + "en" "[{olive}TFDB{default}] Bounce toggle vote {red}failed{default}." + } + + "Dodgeball_BounceVote_Enabled" + { + "en" "[{olive}TFDB{default}] No bounce mode is now {community}enabled{default}." + } + + "Dodgeball_BounceVote_Disabled" + { + "en" "[{olive}TFDB{default}] No bounce mode is now {red}disabled{default}." + } + + "Dodgeball_ClassVote_Cooldown" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Voting for a main rocket class is in cooldown for {darkorange}{1}{default} seconds." + } + + "Dodgeball_ClassVote_Reset" + { + "en" "[{olive}TFDB{default}] The main rocket class has been {red}reset{default}." + } + + "Dodgeball_ClassVote_Changed" + { + "#format" "{1:s}" + "en" "[{olive}TFDB{default}] The main rocket class has been changed to \"{community}{1}{default}\"." + } + + "Dodgeball_CountVote_Cooldown" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Voting for rockets count is in cooldown for {darkorange}{1}{default} seconds." + } + + "Dodgeball_CountVote_Reset" + { + "en" "[{olive}TFDB{default}] The rockets count has been {red}reset{default}." + } + + "Dodgeball_CountVote_Changed" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] The rockets count has been changed to {community}{1}{default}." + } + + "Dodgeball_PresetVote_Cooldown" + { + "#format" "{1:i}" + "en" "[{olive}TFDB{default}] Voting for presets is in cooldown for {darkorange}{1}{default} seconds." + } + + "Dodgeball_PresetVote_NoPresets" + { + "en" "[{olive}TFDB{default}] No presets are configured." + } + + "Dodgeball_PresetVote_Applied" + { + "#format" "{1:s}" + "en" "[{olive}TFDB{default}] Preset changed to {community}{1}{default}." + } +} diff --git a/roles/sourcemod/files/cfg/sourcemod/dodgeball_disable.cfg b/roles/sourcemod/files/cfg/sourcemod/dodgeball_disable.cfg new file mode 100644 index 00000000..930f4057 --- /dev/null +++ b/roles/sourcemod/files/cfg/sourcemod/dodgeball_disable.cfg @@ -0,0 +1,14 @@ +// ---- Config file executes when the Dodgeball game mode is disabled ---- + +// Set the burst ammo to 25 (default) +sm_cvar tf_flamethrower_burstammo 25 + +// Reenable any class restriction / fast respawn plugins here +// ... + +// Enables arena queue +sm_cvar tf_arena_use_queue 1 + +sm_cvar sv_allow_point_servercommand official + +sv_alltalk 0 diff --git a/roles/sourcemod/files/cfg/sourcemod/dodgeball_enable.cfg b/roles/sourcemod/files/cfg/sourcemod/dodgeball_enable.cfg new file mode 100644 index 00000000..78e4c7e4 --- /dev/null +++ b/roles/sourcemod/files/cfg/sourcemod/dodgeball_enable.cfg @@ -0,0 +1,14 @@ +// ---- Config file executes when the Dodgeball game mode is enabled ---- + +// Set the burst ammo to 0 +sm_cvar tf_flamethrower_burstammo 0 + +// Disable any class restriction / fast respawn plugins here +// ... + +// Disables arena queue +sm_cvar tf_arena_use_queue 0 + +sm_cvar sv_allow_point_servercommand always + +sv_alltalk 1 diff --git a/roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_disable.cfg b/roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_disable.cfg new file mode 100644 index 00000000..509f698b --- /dev/null +++ b/roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_disable.cfg @@ -0,0 +1,2 @@ +sm_cvar tf_dodgeball_as_collision 1 +sm_cvar tf_dodgeball_as_damage 1 diff --git a/roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_enable.cfg b/roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_enable.cfg new file mode 100644 index 00000000..ebaf82af --- /dev/null +++ b/roles/sourcemod/files/cfg/sourcemod/dodgeball_ffa_enable.cfg @@ -0,0 +1,2 @@ +sm_cvar tf_dodgeball_as_collision 0 +sm_cvar tf_dodgeball_as_damage 0 diff --git a/roles/srcds/tasks/main.yml b/roles/srcds/tasks/main.yml index 7ab510da..fe277a18 100644 --- a/roles/srcds/tasks/main.yml +++ b/roles/srcds/tasks/main.yml @@ -271,6 +271,30 @@ loop_control: index_var: loop0 +- name: mkdir tf/addons/sourcemod/configs/dodgeball + ansible.builtin.file: + path: ~/srcds-{{ item.server_name_short }}/tf/addons/sourcemod/configs/dodgeball + state: directory + mode: "0755" + tags: + - game_config + loop: "{{ services }}" + when: item.config == "dodgeball" + loop_control: + index_var: loop0 + +- name: Generate /tf/addons/sourcemod/configs/dodgeball/dodgeball_general.cfg + ansible.builtin.template: + src: dodgeball_general.cfg.j2 + dest: ~/srcds-{{ item.server_name_short }}/tf/addons/sourcemod/configs/dodgeball/general.cfg + mode: "0655" + tags: + - game_config + loop: "{{ services }}" + when: item.config == "dodgeball" + loop_control: + index_var: loop0 + - name: Generate /tf/cfg/sourcemod/gbans.cfg ansible.builtin.template: src: gbans.cfg.j2 diff --git a/roles/srcds/templates/dodgeball_general.cfg.j2 b/roles/srcds/templates/dodgeball_general.cfg.j2 new file mode 100644 index 00000000..a8b71b32 --- /dev/null +++ b/roles/srcds/templates/dodgeball_general.cfg.j2 @@ -0,0 +1,313 @@ +// ------------------------------------------------------- +// Events +// ------------------------------------------------------- +// +// In order to further customize the rocket types, you can +// set a command to be executed when a certain event happens +// with a dodgeball rocket. +// +// The events that are right now implemented are the following : +// +// on spawn - When a rocket spawns. +// on deflect - A client has deflected a rocket. +// on kill - One of the rockets has killed the client. +// on explode - Same as on kill, only triggered once. +// on no target - When a rocket has an invalid target. +// - The target parameter "@target" will be the new target of the rocket. +// on destroyed - When a rocket explodes and does not kill anyone. +// - As a result, the rocket entity parameter "@rocket" will always be -1. +// - The target parameter "@target" will be the player who touched the rocket. +// - Requires the extra events plugin. +// +// The possible parameters passed for the commands are the following : +// +// @name - [String] : Name of the projectile type +// @rocket - [Integer] : Rocket entity index +// @owner - [Integer] : Owner client index +// @target - [Integer] : Target client index +// @dead - [Integer] : Last dead client index +// @deflections - [Integer] : Number of rocket deflections +// @speed - [Float] : Speed of the rocket with limit +// @mphspeed - [Integer] : Speed of the rocket in MpH without limit +// @capmphspeed - [Integer] : Speed of the rocket in Mph with limit +// @nocapspeed - [Float] : Speed of the rocket without limit (conversion from MpH) +// @2dspeed - [Float] : Speed of the rocket with limit that has only 2 decimal numbers +// @2dnocapspeed - [Float] : Speed of the rocket without limit that has only 2 decimal numbers +// +// ------------------------------------------------------- +// Commands +// ------------------------------------------------------- +// +// For using with the events system, the plugin has some useful +// commands if you want to make kickass explosion blasts. +// +// tf_dodgeball_explosion +// Shows a huge explosion at the location of the specified client. +// +// tf_dodgeball_shockwave +// Applies a huge shockwave at the location of the client. +// +// If you are able to write plugins, you can include your custom +// commands too! +// +// ------------------------------------------------------- +// Extra information +// ------------------------------------------------------- +// +// It is possible to have map specific configuration files by +// creating a config with the name of the map. Keep in mind that +// no previous values are deleted. +// +// If a keyvalue / parameter has a default value and you do not want +// to change it, you can leave the value empty or ignore the parameter +// altogether. +// +// Neutral rockets cannot damage teammates unless "mp_friendlyfire" +// is set to 1. +// ------------------------------------------------------- + + + +// no airblasting people +// no ammo cvar tf_flamethrower_burstammo 0 +// Disables arena queue +// sm_cvar tf_arena_use_queue 0 +// + +"tf2_dodgeball" +{ + + "general" + { + // >>> Experimental scaling modes (all disabled by default) <<< + // These override specific per-class values when enabled. + // Your existing config will work exactly the same with all of these set to 0. + + "orbit coefficient" "0" // 1 = use "orbit tightness" instead of "turn rate" + "turn rate increment" + // Turn rate is auto-derived from speed, keeping orbit radius constant. + "target speed scaling" "0" // 1 = use "max speed" + "max deflections" instead of "speed increment" + // Speed increment is auto-calculated to reach max speed evenly. + "smooth elevation" "1" // 1 = apply elevation per-frame (smooth) instead of logic timer (~10Hz) steps + // Does not override any values, only changes update rate. + "bounce vertical scale" "0" // 1 = use per-class "bounce vertical ratio" to dampen vertical bounces + + "music" "1" // Play music on Dodgeball gamemode? + + "round start" "" // Music to play on round start (before gameplay start) + "round end (win)" "" // Music to play for the winner team + "round end (lose)" "" // Music to play for the loser team + "gameplay" "" // Music to play when the gameplay starts. This one stops + // at round end. + + "use web player" "0" // If the use of web player is enabled, these will be + "web player url" "" // used instead of the gameplay music + } + + "classes" + { + "common" + { + // >>> Basic parameters <<< + "name" "Homing Rocket" // Full name of the rocket type + "behaviour" "homing" // "homing" for smooth, per-frame updates. + // "legacy homing" for classic, logic timer updates (~10Hz). + + "model" "" // Default: Common rocket model + "is animated" "0" // Only works when using a custom model (Default : 0) + + // Trails section. Only usable if the trails plugin is installed. + // Remove the comments (//) before the keys (ex. "trail particle") if you want to use this section. + + //"trail particle" "" // Particle for particle trail (Default : no custom particle trail) + // Not all particles are going to work + + //"trail sprite" "" // Sprite path for sprite trail (Default : no custom sprite trail) + //"custom color" "" // Custom color for the sprite trail (Default : [255 255 255]) + //"sprite lifetime" "" // Custom lifetime for sprite trail (Default : 1.0 seconds) + //"sprite start width" "" // Custom start width for sprite trail (Default : 6.0 units) + //"sprite end width" "" // Custom end width for sprite trail (Default : 15.0 units) + //"texture resolution" "" // Custom texture resolution (Default : 0.05) + + // You can also insert custom keyvalue pairs for the "env_spritetrail" entity in this section + //"entity keyvalues" + //{ + // For example, this replaces the sprite set in the "trail sprite" key with "materials/sprites/laser.vmt" + // "spritename" "materials/sprites/laser.vmt" + + // Check https://developer.valvesoftware.com/wiki/Env_spritetrail for more info. + //} + + //"remove particles" "" // Remove the default rocket particles? (Default : 0) + //"replace particles" "" // Replace the default rocket particles? Works only if they have been removed (Default : 0) + // Creates a clone of the rocket and adds crit glows + + // Trails section end. + + "play spawn sound" "1" // Does the rocket emit a sound when spawning? (Default : 0) + "play beep sound" "1" // Does the rocket emit a beeping sound? (Default : 0) + "play alert sound" "1" // Does the rocket emit an alert sound to the client when being targetted? (Default : 0) + "spawn sound" "" // Default: Sentry rocket sound + "beep sound" "" // Default: Sentry searching sound + "alert sound" "" // Default: Sentry client spotted sound + "beep interval" "0" // Emit sound every x time (Default : 0.5 seconds) + + // >>> Specific behaviour modificators <<< + "elevate on deflect" "0" // Does the rocket elevate after a deflection? (Default : 0) + "neutral rocket" "0" // Does the rocket have no team based targets? (Default : 0) + "keep direction" "0" // Does the rocket keep its direction after touching a surface? (Default : 0) + "teamless deflects" "0" // Can this rocket be deflected by anyone? Same as neutral but targeting is not affected. (Default : 0) + "reset bounces" "0" // Does this rocket reset its internal bounces count on deflect? (Default : 0) + "no bounce drags" "0" // Can you drag this rocket after it touched a surface? Does not affect "legacy homing" rockets. (Default : 0) + "can be stolen" "0" // Can you steal this rocket from its target? (Default : 0) + "steal team check" "1" // Checks if the stealer and the target are on the same team. (Default : 0) + // Useful for neutral rockets. + + // >>> Movement parameters <<< + "damage" "50" // Base damage done by the rocket. + "damage increment" "25" // Increment per reflection. + // Damage is multiplied by 3 if the rocket is critical. + + "speed" "875" // Base speed for the rocket. + "speed increment" "160" // Speed increment per reflection. + "speed limit" "0" // Speed limit for the rocket (0 to disable speed limiting) + //"max speed" "3500" // Target max speed (used when "target speed scaling" = 1) + //"max deflections" "40" // Deflections to reach max speed (used when "target speed scaling" = 1) + + "turn rate" "0.260" // Turn rate / tick for this rocket (base orbit radius). + // Community sweet spot is often 0.180-0.320 combined with increment. + "turn rate increment" "0.0180" // Increment per reflection. At TF2's max speed cap (~3500 HU/s), + // continuous turn rates of 1.8-3.2 provide the best orbit feel. + "turn rate limit" "0" // Maximum turn rate when deflected (0 to disable turn rate limiting) + // Cannot exceed 1.0 regardless of the actual limit value + //"orbit tightness" "0.3" // Constant orbit coefficient (used when "orbit coefficient" = 1) + // 0.3 matches classic turn rate feel. 0.1 = loose, 1.0 = tight. + + "elevation rate" "0" // Elevation rate when deflected (if enabled) + "elevation limit" "0" // Maximum elevation when deflected (if enabled) + + "control delay" "0" // Delay until the rocket starts tracking the target after a deflection. + + "max bounces" "1000" // How many times can this rocket bounce? + "bounce scale" "0.5" // How hard should the rocket bounce? (Default : 1.0 multiplier) + //"bounce vertical ratio" "1.0" // Scale vertical bounce velocity. 1.0 = full bounce, 0.5 = half height, + // 0.0 = no vertical bounce at all. Lower = flatter bounces. + // (used when "bounce vertical scale" = 1) + //"bounce max vertical speed" "250" // Hard cap on vertical bounce speed in HU/s. No matter how fast + // the rocket is, it can't bounce higher than this. 0 = no cap. + // (used when "bounce vertical scale" = 1) + + // Drag pause: how long the rocket flies straight after a deflect + // before reading the player's aim and resuming homing. + // This is the "drag window" — the brief moment where the rocket + // goes forward and your mouse determines its initial trajectory. + // + // 0.1 = legacy feel (100ms, matches DB_Reborn / default timer rate) + // 0.05 = snappier response (50ms, rocket snaps to aim faster) + // 0.0 = instant (no forward flight, aim read immediately) + // 0.15 = sluggish (150ms, more time to aim but slower feel) + // + // Uses per-frame timing (OnGameFrame, fires every server tick). + // Snapped to tick boundaries internally: e.g. 0.1s on 66-tick + // = 7 ticks (0.106s), on 128-tick = 13 ticks (0.102s). + // + "drag pause duration" "0.1" + + "critical chance" "100" // Percentage of chance for a critical rocket. + + "no. players modifier" "0" // Increment based upon the number of players in the server. + "no. rockets modifier" "0" // Increment based upon the number of rockets fired since the start of the round. + "direction to target weight" "100" // Weight modifier for target selection, based upon the direction of the rocket + // to the client. + + // >>> Events <<< + "on spawn" "" // Actions to execute on rocket spawn. + "on deflect" "" // Actions to execute when a rocket is deflected. + "on kill" "tf_dodgeball_print [{olive}TFDB{default}] {yellow}★ KILL{default} | {lightblue}##@owner## {default}killed {red}##@dead## {default}| Deflects: {darkorange}@deflections {default}| Speed: {red}@capmphspeed mph" // Actions to execute when a rocket kills a client. + "on explode" "" // Actions to execute when a rocket kills a client (triggered once). + "on no target" "" // Actions to execute when a rocket has an invalid target. + "on destroyed" "" // Actions to execute when a rocket explodes (will not trigger if the player is killed by the rocket). + } + + "nuke" + { + // >>> Basic parameters <<< + "name" "Nuke!" + "behaviour" "homing" + "model" "models/custom/dodgeball/nuke/nuke.mdl" + "is animated" "1" + "trail particle" "" + "trail sprite" "" + "custom color" "" + "remove particles" "" + "replace particles" "" + "play spawn sound" "1" + "play beep sound" "1" + "play alert sound" "1" + "spawn sound" "" + "beep sound" "" + "alert sound" "" + "beep interval" "0.2" + + // >>> Specific behaviour modificators <<< + "elevate on deflect" "0" + "neutral rocket" "0" + "keep direction" "1" + "teamless deflects" "0" + "reset bounces" "0" + "no bounce drags" "0" + + // >>> Movement parameters <<< + "damage" "200" + "damage increment" "200" + "speed" "550" + "speed increment" "100" + "speed limit" "0" + "turn rate" "0.233" + "turn rate increment" "0.0275" + "turn rate limit" "0" + "elevation rate" "0.1237" + "elevation limit" "0.1237" + "control delay" "0" + "max bounces" "0" + "bounce scale" "1.0" + "critical chance" "100" + "no. players modifier" "0" + "no. rockets modifier" "0" + "direction to target weight" "25" + + // >>> Events <<< + "on spawn" "" + "on deflect" "" + "on kill" "tf_dodgeball_print [{olive}TFDB{default}] {darkmagenta}☢ NUKE{default} | {lightblue}##@owner## {default}nuked {red}##@dead## {default}| Deflects: {darkorange}@deflections {default}| Speed: {red}@capmphspeed mph" + "on explode" "tf_dodgeball_explosion @dead ; tf_dodgeball_shockwave @dead 200 1000 1000 600" + "on no target" "" + } + } + + "spawners" + { + // >>> Default RED spawner <<< + "red" + { + // >>> Basic parameters <<< + "max rockets" "1" // Max no. of rockets before the spawner can fire another. (Default : 1) + "interval" "2.0" // Minimum time between rocket fires. (Default : 2.0 seconds) + + // >>> Chances table <<< + "common%" "100" // Chance to spawn a common rocket + "nuke%" "0" // Chance to spawn a nuke rocket + } + + // >>> Default BLU spawner <<< + "blu" + { + // >>> Basic parameters <<< + "max rockets" "1" // Max no. of rockets before the spawner can fire another. + "interval" "2.0" // Minimum time between rocket fires. + + // >>> Chances table <<< + "common%" "100" // Chance to spawn a common rocket + "nuke%" "0" // Chance to spawn a nuke rocket + } + } +} diff --git a/roles/srcds/templates/dodgeball_presets.cfg.j2 b/roles/srcds/templates/dodgeball_presets.cfg.j2 new file mode 100644 index 00000000..0a04d235 --- /dev/null +++ b/roles/srcds/templates/dodgeball_presets.cfg.j2 @@ -0,0 +1,56 @@ +// TF2 Dodgeball - Presets Configuration +// +// Presets allow vote plugins to switch between gameplay modes with a single call. +// Each preset defines a rocket class to use, how many rockets spawn, and the spawn interval. +// +// The "rocket class" value must match a class short name (section key) from general.cfg. +// For example, if general.cfg has a class defined as: +// +// "classes" { +// "common" { <-- this is the short name +// "name" "Homing Rocket" +// ... +// } +// } +// +// Then use "rocket class" "common" in the preset. +// +// "spawn interval" is optional. If set to 0 or omitted, the spawner's +// default interval from general.cfg is kept. +// +// Usage from a subplugin: +// int count = TFDB_GetPresetCount(); // how many presets loaded +// TFDB_GetPresetName(0, name, sizeof(name)); // get display name +// TFDB_ApplyPreset(0); // apply preset (destroys rockets, sets spawners) + +"TF2_Dodgeball" +{ + "presets" + { + "1r" + { + "name" "1 Rocket (Competitive)" + "rocket class" "common" + "max rockets" "1" + "spawn interval" "1.5" + } + + // Example additional presets (uncomment and adjust to your classes): + // + // "2r" + // { + // "name" "2 Rockets (Standard)" + // "rocket class" "common" + // "max rockets" "2" + // "spawn interval" "1.5" + // } + // + // "3r" + // { + // "name" "3 Rockets (Casual)" + // "rocket class" "common" + // "max rockets" "3" + // "spawn interval" "2.0" + // } + } +} diff --git a/roles/srcds/templates/motd.txt.j2 b/roles/srcds/templates/motd.txt.j2 index 8eed58be..ee95de09 100644 --- a/roles/srcds/templates/motd.txt.j2 +++ b/roles/srcds/templates/motd.txt.j2 @@ -1 +1,8 @@ -{% if item.config == 'pve' %}{{ motd_pve }}{% else %}{{ motd }}{% endif %} +{% if item.config == 'pve' %} +{{ motd_pve }} +{% elif item.config == 'dodgeball' %} +{{ motd_dodgeball }} +{% else %} +{{ motd }} +{% endif %} +m diff --git a/shell.nix b/shell.nix index d785482c..67945577 100644 --- a/shell.nix +++ b/shell.nix @@ -9,10 +9,13 @@ in pkgs.mkShellNoCC { packages = with pkgs; [ - gnumake ansible ansible-lint - #ansible-language-server yamllint + just + just-lsp + nix + nixd + just-formatter ]; } diff --git a/sm_plugins/TF2-Dodgeball-Modified b/sm_plugins/TF2-Dodgeball-Modified new file mode 160000 index 00000000..effc5875 --- /dev/null +++ b/sm_plugins/TF2-Dodgeball-Modified @@ -0,0 +1 @@ +Subproject commit effc5875cfac501413f68974e7167241d81e8536 diff --git a/sm_plugins_update.sh b/sm_plugins_update.sh index 11f3fc14..d5ad9db2 100755 --- a/sm_plugins_update.sh +++ b/sm_plugins_update.sh @@ -3,6 +3,9 @@ ROOT=$(pwd) SM_ROOT=$ROOT/roles/sourcemod/files/addons/sourcemod SRC_ROOT="sm_plugins" +DODGEBALL_ROOT="$SRC_ROOT/TF2-Dodgeball-Modified" +DODGEBALL_BRANCH="v2.1.0" + STAC_ROOT="$SRC_ROOT/stac" STAC_BRANCH="gbans-native" @@ -45,6 +48,28 @@ HALLOWEENCOSMETICS_BRANCH="master" # git submodule update --init --recursive +pushd $DODGEBALL_ROOT || exit +git fetch --all +git checkout $DODGEBALL_BRANCH +git pull +pushd TF2Dodgeball/addons/sourcemod || exit +for d in 'scripting' 'translations'; do + cp -rv $d "$SM_ROOT" +done +popd || exit + +cp -rv Subplugins/AirblastPrevention/scripting/tfdb_airblast_prevention.sp $SM_ROOT/scripting/ +cp -rv Subplugins/NoBlock/scripting/tfdb_no_block.sp $SM_ROOT/scripting/ +cp -rv Subplugins/Votes/scripting/tfdb_votes.sp $SM_ROOT/scripting/ +cp -rv Subplugins/Speedometer/scripting/tfdb_speedhud.sp $SM_ROOT/scripting/ +cp -rv Subplugins/FFA/scripting/tfdb_ffa.sp $SM_ROOT/scripting/ +cp -rv Subplugins/Print/scripting/tfdb_print.sp $SM_ROOT/scripting/ +#cp -rv TF2Dodgeball/cfg/sourcemod/* $ROOT/roles/sourcemod/files/cfg/sourcemod/ + +popd || exit + +exit 200 + pushd $HALLOWEENCOSMETICS_ROOT || exit git fetch --all git checkout $HALLOWEENCOSMETICS_BRANCH From 3de31dcd04f6b528b6012cc1aa108735cb0c13c1 Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Tue, 3 Mar 2026 16:35:17 -0700 Subject: [PATCH 2/6] Update ball config. Drop extra votes. --- .../addons/sourcemod/scripting/tfdb_votes.sp | 713 ------------------ .../srcds/templates/dodgeball_general.cfg.j2 | 31 +- roles/srcds/templates/motd.txt.j2 | 1 - roles/srcds/templates/server.cfg.j2 | 5 +- sm_plugins_update.sh | 22 +- 5 files changed, 34 insertions(+), 738 deletions(-) delete mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp b/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp deleted file mode 100644 index 62db3584..00000000 --- a/roles/sourcemod/files/addons/sourcemod/scripting/tfdb_votes.sp +++ /dev/null @@ -1,713 +0,0 @@ -#pragma semicolon 1 -#pragma newdecls required - -#include -#include - -#include - -#define PLUGIN_NAME "[TFDB] Votes" -#define PLUGIN_AUTHOR "x07x08" -#define PLUGIN_DESCRIPTION "Various rocket votes." -#define PLUGIN_VERSION "1.1.0" -#define PLUGIN_URL "https://github.com/Silorak/TF2-Dodgeball-Modified" - -int g_iSpawnersCount; - -ConVar CvarVoteBounceDuration; -ConVar CvarVoteClassDuration; -ConVar CvarVoteCountDuration; -ConVar CvarVoteBounceTimeout; -ConVar CvarVoteClassTimeout; -ConVar CvarVoteCountTimeout; -ConVar CvarVotePresetDuration; -ConVar CvarVotePresetTimeout; - -bool VoteBounceAllowed; -bool VoteClassAllowed; -bool VoteCountAllowed; -bool VotePresetAllowed; - -float LastVoteBounceTime; -float LastVoteClassTime; -float LastVoteCountTime; -float LastVotePresetTime; - -bool BounceEnabled; -int MainRocketClass = -1; -int RocketsCount = -1; - -int SavedMaxRockets[MAX_SPAWNER_CLASSES]; - -bool Loaded; - -public Plugin myinfo = -{ - name = PLUGIN_NAME, - author = PLUGIN_AUTHOR, - description = PLUGIN_DESCRIPTION, - version = PLUGIN_VERSION, - url = PLUGIN_URL -}; - -public void OnPluginStart() -{ - LoadTranslations("tfdb.phrases.txt"); - - CvarVoteBounceDuration = CreateConVar("tf_dodgeball_votes_bounce_duration", "20", _, _, true, 0.0); - CvarVoteClassDuration = CreateConVar("tf_dodgeball_votes_class_duration", "20", _, _, true, 0.0); - CvarVoteCountDuration = CreateConVar("tf_dodgeball_votes_count_duration", "20", _, _, true, 0.0); - CvarVoteBounceTimeout = CreateConVar("tf_dodgeball_votes_bounce_timeout", "150", _, _, true, 0.0); - CvarVoteClassTimeout = CreateConVar("tf_dodgeball_votes_class_timeout", "150", _, _, true, 0.0); - CvarVoteCountTimeout = CreateConVar("tf_dodgeball_votes_count_timeout", "150", _, _, true, 0.0); - CvarVotePresetDuration = CreateConVar("tf_dodgeball_votes_preset_duration", "20", _, _, true, 0.0); - CvarVotePresetTimeout = CreateConVar("tf_dodgeball_votes_preset_timeout", "150", _, _, true, 0.0); - - RegConsoleCmd("sm_vrb", CmdVoteBounce, "Start a rocket bounce vote"); - RegConsoleCmd("sm_vrc", CmdVoteClass, "Start a rocket class vote"); - RegConsoleCmd("sm_vrcount", CmdVoteCount, "Start a rocket count vote"); - RegConsoleCmd("sm_vrp", CmdVotePreset, "Start a preset vote"); - RegConsoleCmd("sm_votebounce", CmdVoteBounce, "Start a rocket bounce vote"); - RegConsoleCmd("sm_voteclass", CmdVoteClass, "Start a rocket class vote"); - RegConsoleCmd("sm_votecount", CmdVoteCount, "Start a rocket count vote"); - RegConsoleCmd("sm_votepreset", CmdVotePreset, "Start a preset vote"); - RegConsoleCmd("sm_voterocketbounce", CmdVoteBounce, "Start a rocket bounce vote"); - RegConsoleCmd("sm_voterocketclass", CmdVoteClass, "Start a rocket class vote"); - RegConsoleCmd("sm_voterocketcount", CmdVoteCount, "Start a rocket count vote"); - RegConsoleCmd("sm_voterocketpreset", CmdVotePreset, "Start a preset vote"); - - if (!TFDB_IsDodgeballEnabled()) return; - - char strMapName[64]; GetCurrentMap(strMapName, sizeof(strMapName)); - GetMapDisplayName(strMapName, strMapName, sizeof(strMapName)); - char strMapFile[PLATFORM_MAX_PATH]; FormatEx(strMapFile, sizeof(strMapFile), "%s.cfg", strMapName); - - TFDB_OnRocketsConfigExecuted("general.cfg"); - TFDB_OnRocketsConfigExecuted(strMapFile); -} - -public void OnMapEnd() -{ - if (!Loaded) return; - - VoteBounceAllowed = - VoteClassAllowed = - VoteCountAllowed = - VotePresetAllowed = false; - - LastVoteBounceTime = - LastVoteClassTime = - LastVoteCountTime = - LastVotePresetTime = 0.0; - - BounceEnabled = false; - MainRocketClass = -1; - RocketsCount = -1; - - Loaded = false; - - g_iSpawnersCount = 0; -} - -public void TFDB_OnRocketsConfigExecuted(const char[] strConfigFile) -{ - if (!Loaded) - { - VoteBounceAllowed = - VoteClassAllowed = - VoteCountAllowed = - VotePresetAllowed = true; - - LastVoteBounceTime = - LastVoteClassTime = - LastVoteCountTime = - LastVotePresetTime = 0.0; - - BounceEnabled = false; - MainRocketClass = -1; - RocketsCount = -1; - - Loaded = true; - } - - if (strcmp(strConfigFile, "general.cfg") == 0) - { - g_iSpawnersCount = 0; - } - - ParseConfigurations(strConfigFile); -} - -public Action CmdVoteBounce(int iClient, int iArgs) -{ - if (iClient == 0) - { - ReplyToCommand(iClient, "Command is in-game only."); - - return Plugin_Handled; - } - - if (!TFDB_IsDodgeballEnabled()) - { - CReplyToCommand(iClient, "%t", "Command_Disabled"); - - return Plugin_Handled; - } - - if (IsVoteInProgress()) - { - CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); - - return Plugin_Handled; - } - - if (VoteBounceAllowed) - { - VoteBounceAllowed = false; - LastVoteBounceTime = GetGameTime(); - - StartBounceVote(); - CreateTimer(CvarVoteBounceTimeout.FloatValue, VoteBounceTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); - } - else - { - CReplyToCommand(iClient, "%t", "Dodgeball_BounceVote_Cooldown", - RoundToCeil((LastVoteBounceTime + CvarVoteBounceTimeout.FloatValue) - GetGameTime())); - } - - return Plugin_Handled; -} - -void StartBounceVote() -{ - char strMode[16]; - strMode = !BounceEnabled ? "Enable" : "Disable"; - - Menu hMenu = new Menu(VoteMenuHandler); - hMenu.VoteResultCallback = VoteBounceResultHandler; - - hMenu.SetTitle("%s no rocket bounce mode?", strMode); - - hMenu.AddItem("0", "Yes"); - hMenu.AddItem("1", "No"); - - int iTotal; - int[] iClients = new int[MaxClients]; - - for (int iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) - { - continue; - } - - iClients[iTotal++] = iClient; - } - - hMenu.DisplayVote(iClients, iTotal, CvarVoteBounceDuration.IntValue); -} - -public int VoteMenuHandler(Menu hMenu, MenuAction iMenuActions, int iParam1, int iParam2) -{ - switch (iMenuActions) - { - case MenuAction_End : - { - delete hMenu; - } - } - - return 0; -} - -public void VoteBounceResultHandler(Menu hMenu, - int iNumVotes, - int iNumClients, - const int[][] iClientInfo, - int iNumItems, - const int[][] iItemInfo) -{ - int iWinnerIndex = 0; - - if (iNumItems > 1 && - (iItemInfo[0][VOTEINFO_ITEM_VOTES] == iItemInfo[1][VOTEINFO_ITEM_VOTES])) - { - iWinnerIndex = GetRandomInt(0, 1); - } - - char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); - - if (StrEqual(strWinner, "0")) - { - ToggleBounce(); - } - else - { - CPrintToChatAll("%t", "Dodgeball_BounceVote_Failed"); - } -} - -void ToggleBounce() -{ - if (!BounceEnabled) - { - EnableBounce(); - } - else - { - DisableBounce(); - } -} - -void EnableBounce() -{ - BounceEnabled = true; - - for (int iIndex = 0; iIndex < MAX_ROCKETS; iIndex++) - { - if (!TFDB_IsValidRocket(iIndex)) continue; - - TFDB_SetRocketBounces(iIndex, TFDB_GetRocketClassMaxBounces(TFDB_GetRocketClass(iIndex))); - } - - CPrintToChatAll("%t", "Dodgeball_BounceVote_Enabled"); -} - -void DisableBounce() -{ - BounceEnabled = false; - - for (int iIndex = 0; iIndex < MAX_ROCKETS; iIndex++) - { - if (!TFDB_IsValidRocket(iIndex)) continue; - - TFDB_SetRocketBounces(iIndex, 0); - } - - CPrintToChatAll("%t", "Dodgeball_BounceVote_Disabled"); -} - -public Action CmdVoteClass(int iClient, int iArgs) -{ - if (iClient == 0) - { - ReplyToCommand(iClient, "Command is in-game only."); - - return Plugin_Handled; - } - - if (!TFDB_IsDodgeballEnabled()) - { - CReplyToCommand(iClient, "%t", "Command_Disabled"); - - return Plugin_Handled; - } - - if (IsVoteInProgress()) - { - CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); - - return Plugin_Handled; - } - - if (VoteClassAllowed) - { - VoteClassAllowed = false; - LastVoteClassTime = GetGameTime(); - - StartClassVote(); - CreateTimer(CvarVoteClassTimeout.FloatValue, VoteClassTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); - } - else - { - CReplyToCommand(iClient, "%t", "Dodgeball_ClassVote_Cooldown", - RoundToCeil((LastVoteClassTime + CvarVoteClassTimeout.FloatValue) - GetGameTime())); - } - - return Plugin_Handled; -} - -void StartClassVote() -{ - Menu hMenu = new Menu(VoteMenuHandler); - hMenu.VoteResultCallback = VoteClassResultHandler; - - hMenu.SetTitle("Change main rocket class?"); - - if (MainRocketClass != -1) - { - hMenu.AddItem("-1", "Reset the spawn chances"); - } - - char strClass[8], strRocketClassLongName[32]; - - for (int iClass = 0; iClass < TFDB_GetRocketClassCount(); iClass++) - { - IntToString(iClass, strClass, sizeof(strClass)); - TFDB_GetRocketClassLongName(iClass, strRocketClassLongName, sizeof(strRocketClassLongName)); - - hMenu.AddItem(strClass, strRocketClassLongName, ITEMDRAW_DEFAULT); - } - - int iTotal; - int[] iClients = new int[MaxClients]; - - for (int iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) - { - continue; - } - - iClients[iTotal++] = iClient; - } - - hMenu.DisplayVote(iClients, iTotal, CvarVoteClassDuration.IntValue); -} - -public void VoteClassResultHandler(Menu hMenu, - int iNumVotes, - int iNumClients, - const int[][] iClientInfo, - int iNumItems, - const int[][] iItemInfo) -{ - int iWinnerIndex = 0; - int iClassCount = TFDB_GetRocketClassCount(); - - if (MainRocketClass != -1) iClassCount++; - - bool bEqual = AreVotesEqual(iItemInfo, iClassCount); - - if (bEqual) iWinnerIndex = GetRandomInt(0, (iClassCount - 1)); - - char strWinner[8], strClassLongName[32]; - - hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner), _, strClassLongName, sizeof(strClassLongName)); - - MainRocketClass = StringToInt(strWinner); - - if (MainRocketClass == -1) - { - CPrintToChatAll("%t", "Dodgeball_ClassVote_Reset"); - } - else - { - CPrintToChatAll("%t", "Dodgeball_ClassVote_Changed", strClassLongName); - } - - TFDB_DestroyRockets(); -} - -public Action CmdVoteCount(int iClient, int iArgs) -{ - if (iClient == 0) - { - ReplyToCommand(iClient, "Command is in-game only."); - - return Plugin_Handled; - } - - if (!TFDB_IsDodgeballEnabled()) - { - CReplyToCommand(iClient, "%t", "Command_Disabled"); - - return Plugin_Handled; - } - - if (IsVoteInProgress()) - { - CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); - - return Plugin_Handled; - } - - if (VoteCountAllowed) - { - VoteCountAllowed = false; - LastVoteCountTime = GetGameTime(); - - StartCountVote(); - CreateTimer(CvarVoteCountTimeout.FloatValue, VoteCountTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); - } - else - { - CReplyToCommand(iClient, "%t", "Dodgeball_CountVote_Cooldown", - RoundToCeil((LastVoteCountTime + CvarVoteCountTimeout.FloatValue) - GetGameTime())); - } - - return Plugin_Handled; -} - -void StartCountVote() -{ - Menu hMenu = new Menu(VoteMenuHandler); - hMenu.VoteResultCallback = VoteCountResultHandler; - - hMenu.SetTitle("Change rockets count?"); - - if (RocketsCount != -1) - { - hMenu.AddItem("-1", "Reset rockets count"); - } - - hMenu.AddItem("0", "One rocket"); - hMenu.AddItem("1", "Two rockets"); - hMenu.AddItem("2", "Three rockets"); - hMenu.AddItem("3", "Four rockets"); - hMenu.AddItem("4", "Five rockets"); - - int iTotal; - int[] iClients = new int[MaxClients]; - - for (int iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) - { - continue; - } - - iClients[iTotal++] = iClient; - } - - hMenu.DisplayVote(iClients, iTotal, CvarVoteCountDuration.IntValue); -} - -public void VoteCountResultHandler(Menu hMenu, - int iNumVotes, - int iNumClients, - const int[][] iClientInfo, - int iNumItems, - const int[][] iItemInfo) -{ - int iWinnerIndex = 0; - int iVotesCount = 5; - - if (RocketsCount != -1) iVotesCount++; - - bool bEqual = AreVotesEqual(iItemInfo, iVotesCount); - - if (bEqual) iWinnerIndex = GetRandomInt(0, (iVotesCount - 1)); - - char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); - - RocketsCount = StringToInt(strWinner); - - for (int iIndex = 0; iIndex < TFDB_GetSpawnersCount(); iIndex++) - { - TFDB_SetSpawnersMaxRockets(iIndex, RocketsCount == -1 ? SavedMaxRockets[iIndex] : (RocketsCount + 1)); - } - - if (RocketsCount == -1) - { - CPrintToChatAll("%t", "Dodgeball_CountVote_Reset"); - } - else - { - CPrintToChatAll("%t", "Dodgeball_CountVote_Changed", (RocketsCount + 1)); - } -} - -public Action CmdVotePreset(int iClient, int iArgs) -{ - if (iClient == 0) - { - ReplyToCommand(iClient, "Command is in-game only."); - - return Plugin_Handled; - } - - if (!TFDB_IsDodgeballEnabled()) - { - CReplyToCommand(iClient, "%t", "Command_Disabled"); - - return Plugin_Handled; - } - - if (IsVoteInProgress()) - { - CReplyToCommand(iClient, "%t", "Dodgeball_FFAVote_Conflict"); - - return Plugin_Handled; - } - - if (TFDB_GetPresetCount() == 0) - { - CReplyToCommand(iClient, "%t", "Dodgeball_PresetVote_NoPresets"); - - return Plugin_Handled; - } - - if (VotePresetAllowed) - { - VotePresetAllowed = false; - LastVotePresetTime = GetGameTime(); - - StartPresetVote(); - CreateTimer(CvarVotePresetTimeout.FloatValue, VotePresetTimeoutCallback, _, TIMER_FLAG_NO_MAPCHANGE); - } - else - { - CReplyToCommand(iClient, "%t", "Dodgeball_PresetVote_Cooldown", - RoundToCeil((LastVotePresetTime + CvarVotePresetTimeout.FloatValue) - GetGameTime())); - } - - return Plugin_Handled; -} - -void StartPresetVote() -{ - Menu hMenu = new Menu(VoteMenuHandler); - hMenu.VoteResultCallback = VotePresetResultHandler; - - hMenu.SetTitle("Select gameplay preset:"); - - int iPresetCount = TFDB_GetPresetCount(); - for (int i = 0; i < iPresetCount; i++) - { - char strIndex[8], strName[64]; - IntToString(i, strIndex, sizeof(strIndex)); - TFDB_GetPresetName(i, strName, sizeof(strName)); - hMenu.AddItem(strIndex, strName); - } - - int iTotal; - int[] iClients = new int[MaxClients]; - - for (int iClient = 1; iClient <= MaxClients; iClient++) - { - if (!IsClientInGame(iClient) || IsFakeClient(iClient) || GetClientTeam(iClient) <= 1) - { - continue; - } - - iClients[iTotal++] = iClient; - } - - hMenu.DisplayVote(iClients, iTotal, CvarVotePresetDuration.IntValue); -} - -public void VotePresetResultHandler(Menu hMenu, - int iNumVotes, - int iNumClients, - const int[][] iClientInfo, - int iNumItems, - const int[][] iItemInfo) -{ - int iWinnerIndex = 0; - - bool bEqual = AreVotesEqual(iItemInfo, iNumItems); - - if (bEqual) iWinnerIndex = GetRandomInt(0, (iNumItems - 1)); - - char strWinner[8]; hMenu.GetItem(iItemInfo[iWinnerIndex][VOTEINFO_ITEM_INDEX], strWinner, sizeof(strWinner)); - - int iPreset = StringToInt(strWinner); - - if (TFDB_ApplyPreset(iPreset)) - { - char strName[64]; - TFDB_GetPresetName(iPreset, strName, sizeof(strName)); - CPrintToChatAll("%t", "Dodgeball_PresetVote_Applied", strName); - } -} - -public Action VoteBounceTimeoutCallback(Handle hTimer) -{ - VoteBounceAllowed = true; - - return Plugin_Continue; -} - -public Action VoteClassTimeoutCallback(Handle hTimer) -{ - VoteClassAllowed = true; - - return Plugin_Continue; -} - -public Action VoteCountTimeoutCallback(Handle hTimer) -{ - VoteCountAllowed = true; - - return Plugin_Continue; -} - -public Action VotePresetTimeoutCallback(Handle hTimer) -{ - VotePresetAllowed = true; - - return Plugin_Continue; -} - -public Action TFDB_OnRocketCreatedPre(int iIndex, int &iClass, RocketFlags &iFlags) -{ - if (MainRocketClass == -1) return Plugin_Continue; - - iClass = MainRocketClass; - iFlags = TFDB_GetRocketClassFlags(MainRocketClass); - - return Plugin_Changed; -} - -public void TFDB_OnRocketCreated(int iIndex) -{ - if (!BounceEnabled) return; - - TFDB_SetRocketBounces(iIndex, TFDB_GetRocketClassMaxBounces(TFDB_GetRocketClass(iIndex))); -} - -void ParseConfigurations(const char[] strConfigFile) -{ - char strPath[PLATFORM_MAX_PATH]; - char strFileName[PLATFORM_MAX_PATH]; - FormatEx(strFileName, sizeof(strFileName), "configs/dodgeball/%s", strConfigFile); - BuildPath(Path_SM, strPath, sizeof(strPath), strFileName); - - if (!FileExists(strPath, true)) return; - - KeyValues kvConfig = new KeyValues("TF2_Dodgeball"); - - if (kvConfig.ImportFromFile(strPath) == false) SetFailState("Error while parsing the configuration file."); - - kvConfig.GotoFirstSubKey(); - - do - { - char strSection[64]; kvConfig.GetSectionName(strSection, sizeof(strSection)); - - if (StrEqual(strSection, "spawners")) ParseSpawners(kvConfig); - } - while (kvConfig.GotoNextKey()); - - delete kvConfig; -} - -void ParseSpawners(KeyValues kvConfig) -{ - kvConfig.GotoFirstSubKey(); - - do - { - int iIndex = g_iSpawnersCount; - - SavedMaxRockets[iIndex] = kvConfig.GetNum("max rockets", 1); - - g_iSpawnersCount++; - } - while (kvConfig.GotoNextKey()); - - kvConfig.GoBack(); -} - -bool AreVotesEqual(const int[][] iVoteItems, int iSize) -{ - int iFirst = iVoteItems[0][VOTEINFO_ITEM_VOTES]; - - for (int iIndex = 1; iIndex < iSize; iIndex++) - { - if (iVoteItems[iIndex][VOTEINFO_ITEM_VOTES] != iFirst) return false; - } - - return true; -} diff --git a/roles/srcds/templates/dodgeball_general.cfg.j2 b/roles/srcds/templates/dodgeball_general.cfg.j2 index a8b71b32..eb1854fe 100644 --- a/roles/srcds/templates/dodgeball_general.cfg.j2 +++ b/roles/srcds/templates/dodgeball_general.cfg.j2 @@ -66,14 +66,6 @@ // is set to 1. // ------------------------------------------------------- - - -// no airblasting people -// no ammo cvar tf_flamethrower_burstammo 0 -// Disables arena queue -// sm_cvar tf_arena_use_queue 0 -// - "tf2_dodgeball" { @@ -91,7 +83,7 @@ // Does not override any values, only changes update rate. "bounce vertical scale" "0" // 1 = use per-class "bounce vertical ratio" to dampen vertical bounces - "music" "1" // Play music on Dodgeball gamemode? + "music" "0" // Play music on Dodgeball gamemode? "round start" "" // Music to play on round start (before gameplay start) "round end (win)" "" // Music to play for the winner team @@ -154,9 +146,9 @@ // >>> Specific behaviour modificators <<< "elevate on deflect" "0" // Does the rocket elevate after a deflection? (Default : 0) "neutral rocket" "0" // Does the rocket have no team based targets? (Default : 0) - "keep direction" "0" // Does the rocket keep its direction after touching a surface? (Default : 0) + "keep direction" "1" // Does the rocket keep its direction after touching a surface? (Default : 0) "teamless deflects" "0" // Can this rocket be deflected by anyone? Same as neutral but targeting is not affected. (Default : 0) - "reset bounces" "0" // Does this rocket reset its internal bounces count on deflect? (Default : 0) + "reset bounces" "1" // Does this rocket reset its internal bounces count on deflect? (Default : 0) "no bounce drags" "0" // Can you drag this rocket after it touched a surface? Does not affect "legacy homing" rockets. (Default : 0) "can be stolen" "0" // Can you steal this rocket from its target? (Default : 0) "steal team check" "1" // Checks if the stealer and the target are on the same team. (Default : 0) @@ -188,7 +180,9 @@ "control delay" "0" // Delay until the rocket starts tracking the target after a deflection. "max bounces" "1000" // How many times can this rocket bounce? - "bounce scale" "0.5" // How hard should the rocket bounce? (Default : 1.0 multiplier) + "bounce scale" "1.0" // How hard should the rocket bounce? (Default : 1.0 multiplier) + "bounce force scale" "1.5" + "bounce force angle" "45.0" //"bounce vertical ratio" "1.0" // Scale vertical bounce velocity. 1.0 = full bounce, 0.5 = half height, // 0.0 = no vertical bounce at all. Lower = flatter bounces. // (used when "bounce vertical scale" = 1) @@ -196,6 +190,14 @@ // the rocket is, it can't bounce higher than this. 0 = no cap. // (used when "bounce vertical scale" = 1) + "crawl bounce" "1" // Use crawl bounce scaling/clamping. + "crawl bounce scale" "1" // Extra Z scaling in crawl mode. + "crawl bounce max up" "1500" + + "no. players modifier" "0" // Increment based upon the number of players in the server. + "no. rockets modifier" "0" // Increment based upon the number of rockets fired since the start of the round. + "direction to target weight" "100" // Weight modifier for target selection, based upon the direction of the rocket + // to the client. // Drag pause: how long the rocket flies straight after a deflect // before reading the player's aim and resuming homing. // This is the "drag window" — the brief moment where the rocket @@ -210,7 +212,7 @@ // Snapped to tick boundaries internally: e.g. 0.1s on 66-tick // = 7 ticks (0.106s), on 128-tick = 13 ticks (0.102s). // - "drag pause duration" "0.1" + "drag pause duration" "0.05" "critical chance" "100" // Percentage of chance for a critical rocket. @@ -222,7 +224,8 @@ // >>> Events <<< "on spawn" "" // Actions to execute on rocket spawn. "on deflect" "" // Actions to execute when a rocket is deflected. - "on kill" "tf_dodgeball_print [{olive}TFDB{default}] {yellow}★ KILL{default} | {lightblue}##@owner## {default}killed {red}##@dead## {default}| Deflects: {darkorange}@deflections {default}| Speed: {red}@capmphspeed mph" // Actions to execute when a rocket kills a client. + "on kill" "" + // "on kill" "tf_dodgeball_print [{olive}TFDB{default}] {yellow}★ KILL{default} | {lightblue}##@owner## {default}killed {red}##@dead## {default}| Deflects: {darkorange}@deflections {default}| Speed: {red}@capmphspeed mph" // Actions to execute when a rocket kills a client. "on explode" "" // Actions to execute when a rocket kills a client (triggered once). "on no target" "" // Actions to execute when a rocket has an invalid target. "on destroyed" "" // Actions to execute when a rocket explodes (will not trigger if the player is killed by the rocket). diff --git a/roles/srcds/templates/motd.txt.j2 b/roles/srcds/templates/motd.txt.j2 index ee95de09..7a001c3a 100644 --- a/roles/srcds/templates/motd.txt.j2 +++ b/roles/srcds/templates/motd.txt.j2 @@ -5,4 +5,3 @@ {% else %} {{ motd }} {% endif %} -m diff --git a/roles/srcds/templates/server.cfg.j2 b/roles/srcds/templates/server.cfg.j2 index 74326fab..c9156fd2 100644 --- a/roles/srcds/templates/server.cfg.j2 +++ b/roles/srcds/templates/server.cfg.j2 @@ -148,9 +148,12 @@ sv_disable_weapon_drop_on_death 1 {#{% endfor %}#} // Built in class limit commands + +{% if item.tf_classlimit|default(3) > 0 %} tf_classlimit {{ item.tf_classlimit|default(3) }} sv_vote_issue_classlimits_allowed 0 sv_vote_issue_classlimits_max 3 +{% endif %} // Disable stalemates mp_stalemate_enable 0 @@ -228,4 +231,4 @@ tv_snapshotrate {{ tv_snapshotrate|default(64) }} mp_timelimit 0 mp_winlimit 0 mp_maxrounds 0 -{% endif %} \ No newline at end of file +{% endif %} diff --git a/sm_plugins_update.sh b/sm_plugins_update.sh index d5ad9db2..f9db69d2 100755 --- a/sm_plugins_update.sh +++ b/sm_plugins_update.sh @@ -60,7 +60,7 @@ popd || exit cp -rv Subplugins/AirblastPrevention/scripting/tfdb_airblast_prevention.sp $SM_ROOT/scripting/ cp -rv Subplugins/NoBlock/scripting/tfdb_no_block.sp $SM_ROOT/scripting/ -cp -rv Subplugins/Votes/scripting/tfdb_votes.sp $SM_ROOT/scripting/ +#cp -rv Subplugins/Votes/scripting/tfdb_votes.sp $SM_ROOT/scripting/ cp -rv Subplugins/Speedometer/scripting/tfdb_speedhud.sp $SM_ROOT/scripting/ cp -rv Subplugins/FFA/scripting/tfdb_ffa.sp $SM_ROOT/scripting/ cp -rv Subplugins/Print/scripting/tfdb_print.sp $SM_ROOT/scripting/ @@ -68,8 +68,6 @@ cp -rv Subplugins/Print/scripting/tfdb_print.sp $SM_ROOT/scripting/ popd || exit -exit 200 - pushd $HALLOWEENCOSMETICS_ROOT || exit git fetch --all git checkout $HALLOWEENCOSMETICS_BRANCH @@ -129,12 +127,6 @@ git checkout $ECON_DATA_BRANCH cp -rv scripting gamedata "$SM_ROOT" popd || exit -pushd $ATTRIBUTES_ROOT || exit -git fetch --all -git checkout $ATTRIBUTES_BRANCH -cp -rv scripting gamedata "$SM_ROOT" -rm "$SM_ROOT/scripting/tf2attributes_example.sp" -popd || exit pushd $CENTERPROJECTILES_ROOT || exit git fetch --all @@ -163,3 +155,15 @@ git fetch --all git checkout $PVE_BRANCH cp -rv scripting gamedata "$SM_ROOT" popd || exit + + +echo "tf2attributes skipped" + +exit + +pushd $ATTRIBUTES_ROOT || exit +git fetch --all +git checkout $ATTRIBUTES_BRANCH +cp -rv scripting gamedata "$SM_ROOT" +rm "$SM_ROOT/scripting/tf2attributes_example.sp" +popd || exit From 944d4c3e157acd2af0f6584dd1da320bd2374aca Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Tue, 3 Mar 2026 16:35:23 -0700 Subject: [PATCH 3/6] Add fov plugin --- .../addons/sourcemod/gamedata/fov.games.txt | 65 +++++ .../files/addons/sourcemod/scripting/fov.sp | 237 ++++++++++++++++++ .../sourcemod/translations/fov.phrases.txt | 17 ++ 3 files changed, 319 insertions(+) create mode 100644 roles/sourcemod/files/addons/sourcemod/gamedata/fov.games.txt create mode 100644 roles/sourcemod/files/addons/sourcemod/scripting/fov.sp create mode 100644 roles/sourcemod/files/addons/sourcemod/translations/fov.phrases.txt diff --git a/roles/sourcemod/files/addons/sourcemod/gamedata/fov.games.txt b/roles/sourcemod/files/addons/sourcemod/gamedata/fov.games.txt new file mode 100644 index 00000000..5839dd1c --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/gamedata/fov.games.txt @@ -0,0 +1,65 @@ +"Games" +{ + "tf" + { + "Functions" + { + "CBasePlayer::SetFOV" + { + "signature" "CBasePlayer::SetFOV" + "callconv" "thiscall" + "return" "bool" + "this" "entity" + + "arguments" + { + "pRequester" + { + "type" "cbaseentity" + } + "FOV" + { + "type" "int" + } + "zoomRate" + { + "type" "float" + } + "iZoomStart" + { + "type" "int" + } + } + } + "CBasePlayer::SetDefaultFOV" + { + "signature" "CBasePlayer::SetDefaultFOV" + "callconv" "thiscall" + "return" "void" + "this" "entity" + + "arguments" + { + "fov" + { + "type" "int" + } + } + } + } + + "Signatures" + { + "CBasePlayer::SetFOV" + { + "library" "server" + "linux" "@_ZN11CBasePlayer6SetFOVEP11CBaseEntityifi" + } + "CBasePlayer::SetDefaultFOV" + { + "library" "server" + "linux" "@_ZN11CBasePlayer13SetDefaultFOVEi" + } + } + } +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/fov.sp b/roles/sourcemod/files/addons/sourcemod/scripting/fov.sp new file mode 100644 index 00000000..9f3ef2ad --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/scripting/fov.sp @@ -0,0 +1,237 @@ +/** + * Copyright Andrew Betson. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include +#include + +#pragma semicolon 1 +#pragma newdecls required + +ConVar sv_fov_min; +ConVar sv_fov_max; +int g_nFOVOverride[ MAXPLAYERS + 1 ] = { -1, ... }; +Handle g_hCookie_FOV; + +DHookSetup g_hDetour_BasePlayer_SetFOV; +DHookSetup g_hDetour_BasePlayer_SetDefaultFOV; + +public Plugin myinfo = +{ + name = "[TF2] FOV", + author = "Andrew \"andrewb\" Betson", + description = "Allow players to set their FOV beyond the arbitrary limit of the fov_desired cvar.", + version = "1.0.2", + url = "https://github.com/AndrewBetson/TF-FOV" +}; + +public void OnPluginStart() +{ + LoadTranslations( "common.phrases" ); + LoadTranslations( "fov.phrases" ); + + sv_fov_min = CreateConVar( "sv_fov_min", "75.0", "Minimum FOV", FCVAR_NOTIFY, true, 10.0, true, 998.0 ); + sv_fov_min.AddChangeHook( ConVar_FOV_MinMax ); + + sv_fov_max = CreateConVar( "sv_fov_max", "120.0", "Maximum FOV", FCVAR_NOTIFY, true, 11.0, true, 999.0 ); + sv_fov_max.AddChangeHook( ConVar_FOV_MinMax ); + + RegConsoleCmd( "sm_fov", Cmd_FOV, "Set calling players FOV" ); + RegConsoleCmd( "sm_fov_clear", Cmd_FOV_Clear, "Clear calling players FOV preference" ); + + AutoExecConfig( true, "fov" ); + + g_hCookie_FOV = RegClientCookie( "fov_override", "FOV", CookieAccess_Public ); + + Handle hGameData = LoadGameConfigFile( "fov.games" ); + if ( !hGameData ) + { + SetFailState( "Failed to load fov gamedata." ); + } + + g_hDetour_BasePlayer_SetFOV = DHookCreateFromConf( hGameData, "CBasePlayer::SetFOV" ); + g_hDetour_BasePlayer_SetDefaultFOV = DHookCreateFromConf( hGameData, "CBasePlayer::SetDefaultFOV" ); + + delete hGameData; + + if ( !DHookEnableDetour( g_hDetour_BasePlayer_SetFOV, false, Detour_BasePlayer_SetFOV ) ) + { + SetFailState( "Failed to detour CBasePlayer::SetFOV, tell Andrew to update the signatures." ); + } + + if ( !DHookEnableDetour( g_hDetour_BasePlayer_SetDefaultFOV, false, Detour_BasePlayer_SetDefaultFOV ) ) + { + SetFailState( "Failed to detour CBasePlayer::SetDefaultFOV, tell Andrew to update the signatures." ); + } + + // Late-load/reload support. + for ( int i = 1; i <= MaxClients; i++ ) + { + if ( IsClientInGame( i ) ) + { + if ( AreClientCookiesCached( i ) ) + { + OnClientCookiesCached( i ); + } + } + } +} + +public void OnClientCookiesCached( int nClientIdx ) +{ + if ( nClientIdx <= 0 ) + { + return; + } + + char szFOV[ 4 ]; + GetClientCookie( nClientIdx, g_hCookie_FOV, szFOV, 4 ); + + if ( szFOV[ 0 ] == EOS ) + { + g_nFOVOverride[ nClientIdx ] = -1; + return; + } + + g_nFOVOverride[ nClientIdx ] = StringToInt( szFOV ); +} + +public void OnClientDisconnect( int nClientIdx ) +{ + g_nFOVOverride[ nClientIdx ] = -1; +} + +public Action Cmd_FOV( int nClientIdx, int nNumArgs ) +{ + if ( nNumArgs < 1 ) + { + CReplyToCommand( nClientIdx, "%t", "FOV_Usage" ); + return Plugin_Continue; + } + + int nNewFOV; + +#if SOURCEMOD_V_MINOR >= 11 + if ( !GetCmdArgIntEx( 1, nNewFOV ) ) + { + CReplyToCommand( nClientIdx, "%t", "FOV_MustBeANumber" ); + return Plugin_Continue; + } +#else + char szSM110Hack[ 4 ]; + GetCmdArg( 1, szSM110Hack, 4 ); + + nNewFOV = StringToInt( szSM110Hack ); +#endif // SOURCEMOD_V_MINOR == 11 + + if ( nNewFOV > sv_fov_max.IntValue || nNewFOV < sv_fov_min.IntValue ) + { + CReplyToCommand( nClientIdx, "%t", "FOV_MustBeWithinRange", sv_fov_min.IntValue, sv_fov_max.IntValue ); + return Plugin_Continue; + } + + g_nFOVOverride[ nClientIdx ] = nNewFOV; + + char szNewFOV[ 4 ]; + IntToString( nNewFOV, szNewFOV, 4 ); + + SetClientCookie( nClientIdx, g_hCookie_FOV, szNewFOV ); + SetClientFOV( nClientIdx ); + + return Plugin_Continue; +} + +public Action Cmd_FOV_Clear( int nClientIdx, int nNumArgs ) +{ + g_nFOVOverride[ nClientIdx ] = -1; + + char szNullFOV[ 4 ]; + IntToString( -1, szNullFOV, 4 ); + + SetClientCookie( nClientIdx, g_hCookie_FOV, szNullFOV ); + + return Plugin_Continue; +} + +public void ConVar_FOV_MinMax( ConVar hConVar, const char[] szOldValue, const char[] szNewValue ) +{ + int nNewValue = StringToInt( szNewValue ); + + for ( int i = 1; i <= MaxClients; i++ ) + { + if ( IsClientInGame( i ) ) + { + if ( ( g_nFOVOverride[ i ] > nNewValue && hConVar == sv_fov_max ) || ( g_nFOVOverride[ i ] < nNewValue && hConVar == sv_fov_min ) ) + { + g_nFOVOverride[ i ] = nNewValue; + SetClientFOV( i ); + } + } + } +} + +public MRESReturn Detour_BasePlayer_SetFOV( int pBasePlayer, DHookReturn hReturn, DHookParam hParams ) +{ + if ( g_nFOVOverride[ pBasePlayer ] == -1 ) + { + return MRES_Ignored; + } + + RequestFrame( Frame_SetFOV, pBasePlayer ); + + return MRES_Ignored; +} + +public MRESReturn Detour_BasePlayer_SetDefaultFOV( int pBasePlayer, DHookParam hParams ) +{ + if ( g_nFOVOverride[ pBasePlayer ] == -1 ) + { + return MRES_Ignored; + } + + RequestFrame( Frame_SetFOV, pBasePlayer ); + + return MRES_Ignored; +} + +void Frame_SetFOV( any aData ) +{ + int pBasePlayer = view_as< int >( aData ); + + if ( TF2_IsPlayerInCondition( pBasePlayer, TFCond_Zoomed ) ) + { + return; + } + + SetClientFOV( pBasePlayer ); +} + +void SetClientFOV( int nClientIdx ) +{ + int nTargetFOV = g_nFOVOverride[ nClientIdx ]; + + // Clamp the target FOV to the min-max range. + if ( nTargetFOV > sv_fov_max.IntValue ) nTargetFOV = sv_fov_max.IntValue; + if ( nTargetFOV < sv_fov_min.IntValue ) nTargetFOV = sv_fov_min.IntValue; + + SetEntProp( nClientIdx, Prop_Send, "m_iFOV", nTargetFOV ); + SetEntProp( nClientIdx, Prop_Send, "m_iDefaultFOV", nTargetFOV ); +} diff --git a/roles/sourcemod/files/addons/sourcemod/translations/fov.phrases.txt b/roles/sourcemod/files/addons/sourcemod/translations/fov.phrases.txt new file mode 100644 index 00000000..ec9b7903 --- /dev/null +++ b/roles/sourcemod/files/addons/sourcemod/translations/fov.phrases.txt @@ -0,0 +1,17 @@ +"Phrases" +{ + "FOV_Usage" + { + "en" "{orange}[FOV]{default} Usage: sm_fov " + } + + "FOV_MustBeANumber" + { + "en" "{orange}[FOV]{default} Parameter must be a number." + } + + "FOV_MustBeWithinRange" + { + "en" "{orange}[FOV]{default} Parameter must be within range %d-%d" + } +} From e47a2f521cc6de88dc67b5c6f126d4dbb41ea963 Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Tue, 3 Mar 2026 21:34:13 -0700 Subject: [PATCH 4/6] Use UDL fork. --- .../addons/sourcemod/scripting/dodgeball.sp | 65 ++- .../scripting/include/dodgeball_config.inc | 91 ++--- .../scripting/include/dodgeball_core.inc | 18 +- .../scripting/include/dodgeball_events.inc | 62 +-- .../scripting/include/dodgeball_natives.inc | 72 +--- .../scripting/include/dodgeball_rockets.inc | 371 ++++++++---------- .../sourcemod/scripting/include/tfdb.inc | 106 ++--- .../sourcemod/translations/tfdb.phrases.txt | 22 -- roles/srcds/tasks/main.yml | 14 +- .../srcds/templates/dodgeball_general.cfg.j2 | 55 +-- sm_plugins/TF2-Dodgeball-Modified | 1 - sm_plugins/TF2-Dodgeball-Modified-UDL | 1 + sm_plugins_update.sh | 6 +- 13 files changed, 338 insertions(+), 546 deletions(-) delete mode 160000 sm_plugins/TF2-Dodgeball-Modified create mode 160000 sm_plugins/TF2-Dodgeball-Modified-UDL diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp b/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp index e1575846..50414d73 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp +++ b/roles/sourcemod/files/addons/sourcemod/scripting/dodgeball.sp @@ -19,7 +19,7 @@ // ********************************************************************************* #define PLUGIN_NAME "[TF2] Dodgeball" #define PLUGIN_AUTHOR "Damizean, x07x08 continued by Silorak" -#define PLUGIN_VERSION "2.1.0" +#define PLUGIN_VERSION "2.0.2" #define PLUGIN_CONTACT "https://github.com/Silorak/TF2-Dodgeball-Modified" enum Musics @@ -48,7 +48,9 @@ ConVar CvarDelayPreventionSpeedup; ConVar CvarNoTargetRedirectDamage; ConVar CvarStealMessage; ConVar CvarDelayMessage; - +// New CVar for bounce mechanic +ConVar CvarBounceForceAngle; +ConVar CvarBounceForceScale; // -----<<< Gameplay >>>----- @@ -61,20 +63,13 @@ float NextSpawnTime; int LastDeadTeam; int LastDeadClient; int PlayerCount; +float TickModifier; int LastStealer; -// Cached SendProp offset for rocket damage (m_iDeflected + 4). -// Looked up once at plugin start instead of calling FindSendPropInfo every frame. -int DamageOffset; - eRocketSteal StealInfo[MAXPLAYERS + 1]; // -----<<< Configuration >>>----- bool MusicEnabled; -bool UseOrbitCoefficient; -bool UseTargetSpeedScaling; -bool UseSmoothElevation; -bool UseBounceVerticalScale; bool Music[view_as(SizeOfMusicsArray)]; char MusicPath[view_as(SizeOfMusicsArray)][PLATFORM_MAX_PATH]; bool UseWebPlayer; @@ -97,9 +92,6 @@ float RocketLastDeflectionTime[MAX_ROCKETS]; float RocketLastBeepTime[MAX_ROCKETS]; float LastSpawnTime[MAX_ROCKETS]; int RocketBounces[MAX_ROCKETS]; -bool RocketHomingPaused[MAX_ROCKETS]; -bool RocketIsDragPause[MAX_ROCKETS]; // true = drag pause (per-frame unpause), false = bounce pause (timer unpause) -float RocketDragPauseEnd[MAX_ROCKETS]; // GetGameTime() when drag pause should end int RocketCount; // Classes @@ -126,6 +118,8 @@ float RocketClassElevationLimit[MAX_ROCKET_CLASSES]; float RocketClassRocketsModifier[MAX_ROCKET_CLASSES]; float RocketClassPlayerModifier[MAX_ROCKET_CLASSES]; float RocketClassControlDelay[MAX_ROCKET_CLASSES]; +float RocketClassDragTimeMin[MAX_ROCKET_CLASSES]; +float RocketClassDragTimeMax[MAX_ROCKET_CLASSES]; float RocketClassTargetWeight[MAX_ROCKET_CLASSES]; DataPack RocketClassCmdsOnSpawn[MAX_ROCKET_CLASSES]; DataPack RocketClassCmdsOnDeflect[MAX_ROCKET_CLASSES]; @@ -134,12 +128,10 @@ DataPack RocketClassCmdsOnExplode[MAX_ROCKET_CLASSES]; DataPack RocketClassCmdsOnNoTarget[MAX_ROCKET_CLASSES]; int RocketClassMaxBounces[MAX_ROCKET_CLASSES]; float RocketClassBounceScale[MAX_ROCKET_CLASSES]; -float RocketClassOrbitTightness[MAX_ROCKET_CLASSES]; -float RocketClassMaxSpeed[MAX_ROCKET_CLASSES]; -int RocketClassMaxDeflections[MAX_ROCKET_CLASSES]; -float RocketClassBounceVerticalScale[MAX_ROCKET_CLASSES]; -float RocketClassBounceMaxVerticalSpeed[MAX_ROCKET_CLASSES]; -float RocketClassDragPauseDuration[MAX_ROCKET_CLASSES]; +float RocketClassCrawlBounceScale[MAX_ROCKET_CLASSES]; +float RocketClassCrawlBounceMaxUp[MAX_ROCKET_CLASSES]; +float RocketClassBounceForceAngle[MAX_ROCKET_CLASSES]; +float RocketClassBounceForceScale[MAX_ROCKET_CLASSES]; int RocketClassCount; // Spawner classes @@ -163,13 +155,6 @@ int SpawnPointsBluEntity[MAX_SPAWN_POINTS]; int DefaultRedSpawner; int DefaultBluSpawner; -// -----<<< Presets >>>----- -char PresetName[MAX_PRESETS][64]; -char PresetRocketClass[MAX_PRESETS][16]; -int PresetMaxRockets[MAX_PRESETS]; -float PresetSpawnInterval[MAX_PRESETS]; -int PresetCount; - // -----<<< Forward handles >>>----- Handle ForwardOnRocketCreated; Handle ForwardOnRocketCreatedPre; @@ -186,12 +171,12 @@ Handle ForwardOnRocketStateChanged; // ********************************************************************************* // PLUGIN LOGIC (INCLUDES) // ********************************************************************************* -#include "dodgeball_utilities.inc" -#include "dodgeball_config.inc" -#include "dodgeball_rockets.inc" -#include "dodgeball_events.inc" -#include "dodgeball_core.inc" -#include "dodgeball_natives.inc" +#include +#include +#include +#include +#include +#include // ********************************************************************************* // PLUGIN INFO & LIFECYCLE @@ -225,14 +210,12 @@ public void OnPluginStart() CvarNoTargetRedirectDamage = CreateConVar("tf_dodgeball_redirect_damage", "1", "Reduce all damage when a rocket has an invalid target?", _, true, 0.0, true, 1.0); CvarStealMessage = CreateConVar("tf_dodgeball_sp_message", "1", "Display the steal message(s)?", _, true, 0.0, true, 1.0); CvarDelayMessage = CreateConVar("tf_dodgeball_dp_message", "1", "Display the delay message(s)?", _, true, 0.0, true, 1.0); - + CvarBounceForceAngle = CreateConVar("tf_dodgeball_bounce_force_angle", "45.0", "Minimum downward angle (pitch) for a player to trigger a forced bounce.", _, true, 0.0, true, 90.0); + CvarBounceForceScale = CreateConVar("tf_dodgeball_bounce_force_scale", "1.5", "How much stronger a player-forced bounce is. (Multiplier)", _, true, 1.0); SpawnersTrie = new StringMap(); - - // Cache the SendProp offset for rocket damage once at plugin start. - // This is m_iDeflected + 4 bytes, used to set rocket damage via SetEntDataFloat. - DamageOffset = FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4; + TickModifier = 0.1 / GetTickInterval(); AddTempEntHook("TFExplosion", OnTFExplosion); @@ -286,7 +269,10 @@ public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] strError, int iE CreateNative("TFDB_SetRocketClassPlayerModifier", Native_SetRocketClassPlayerModifier); CreateNative("TFDB_GetRocketClassControlDelay", Native_GetRocketClassControlDelay); CreateNative("TFDB_SetRocketClassControlDelay", Native_SetRocketClassControlDelay); - + CreateNative("TFDB_GetRocketClassDragTimeMin", Native_GetRocketClassDragTimeMin); + CreateNative("TFDB_SetRocketClassDragTimeMin", Native_SetRocketClassDragTimeMin); + CreateNative("TFDB_GetRocketClassDragTimeMax", Native_GetRocketClassDragTimeMax); + CreateNative("TFDB_SetRocketClassDragTimeMax", Native_SetRocketClassDragTimeMax); CreateNative("TFDB_SetRocketClassCount", Native_SetRocketClassCount); CreateNative("TFDB_SetRocketEntity", Native_SetRocketEntity); CreateNative("TFDB_GetRocketClassMaxBounces", Native_GetRocketClassMaxBounces); @@ -382,9 +368,6 @@ public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] strError, int iE CreateNative("TFDB_SetRocketState", Native_SetRocketState); CreateNative("TFDB_GetStealInfo", Native_GetStealInfo); CreateNative("TFDB_SetStealInfo", Native_SetStealInfo); - CreateNative("TFDB_GetPresetCount", Native_GetPresetCount); - CreateNative("TFDB_GetPresetName", Native_GetPresetName); - CreateNative("TFDB_ApplyPreset", Native_ApplyPreset); SetupForwards(); diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc index bb25aa48..d94cf83f 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_config.inc @@ -315,7 +315,6 @@ void ParseConfigurations(char[] strConfigFile = "general.cfg") if (StrEqual(strSection, "general")) ParseGeneral(kvConfig); else if (StrEqual(strSection, "classes")) ParseClasses(kvConfig); else if (StrEqual(strSection, "spawners")) ParseSpawners(kvConfig); - else if (StrEqual(strSection, "presets")) ParsePresets(kvConfig); } while (kvConfig.GotoNextKey()); @@ -323,15 +322,9 @@ void ParseConfigurations(char[] strConfigFile = "general.cfg") Forward_OnRocketsConfigExecuted(strConfigFile); } -/** Parses the "general" section of the config (music and scaling settings). */ +/** Parses the "general" section of the config (music settings). */ void ParseGeneral(KeyValues kvConfig) { - // Scaling mode flags (read before music to avoid early return) - UseOrbitCoefficient = view_as(kvConfig.GetNum("orbit coefficient", 0)); - UseTargetSpeedScaling = view_as(kvConfig.GetNum("target speed scaling", 0)); - UseSmoothElevation = view_as(kvConfig.GetNum("smooth elevation", 0)); - UseBounceVerticalScale = view_as(kvConfig.GetNum("bounce vertical scale", 0)); - MusicEnabled = view_as(kvConfig.GetNum("music", 0)); if (!MusicEnabled) return; @@ -400,14 +393,15 @@ void ParseClasses(KeyValues kvConfig) } } - if (kvConfig.GetNum("elevate on deflect", 0) == 1) iFlags |= RocketFlag_ElevateOnDeflect; - if (kvConfig.GetNum("neutral rocket", 0) == 1) iFlags |= RocketFlag_IsNeutral; - if (kvConfig.GetNum("keep direction", 0) == 1) iFlags |= RocketFlag_KeepDirection; - if (kvConfig.GetNum("teamless deflects", 0) == 1) iFlags |= RocketFlag_TeamlessHits; - if (kvConfig.GetNum("reset bounces", 0) == 1) iFlags |= RocketFlag_ResetBounces; - if (kvConfig.GetNum("no bounce drags", 0) == 1) iFlags |= RocketFlag_NoBounceDrags; - if (kvConfig.GetNum("can be stolen", 0) == 1) iFlags |= RocketFlag_CanBeStolen; - if (kvConfig.GetNum("steal team check", 0) == 1) iFlags |= RocketFlag_StealTeamCheck; + if (kvConfig.GetNum("elevate on deflect", 1) == 1) iFlags |= RocketFlag_ElevateOnDeflect; + if (kvConfig.GetNum("neutral rocket", 0) == 1) iFlags |= RocketFlag_IsNeutral; + if (kvConfig.GetNum("keep direction", 0) == 1) iFlags |= RocketFlag_KeepDirection; + if (kvConfig.GetNum("teamless deflects", 0) == 1) iFlags |= RocketFlag_TeamlessHits; + if (kvConfig.GetNum("reset bounces", 0) == 1) iFlags |= RocketFlag_ResetBounces; + if (kvConfig.GetNum("no bounce drags", 0) == 1) iFlags |= RocketFlag_NoBounceDrags; + if (kvConfig.GetNum("can be stolen", 0) == 1) iFlags |= RocketFlag_CanBeStolen; + if (kvConfig.GetNum("steal team check", 0) == 1) iFlags |= RocketFlag_StealTeamCheck; + if (kvConfig.GetNum("crawl bounce", 0) == 1) iFlags |= RocketFlag_CrawlBounce; RocketClassDamage[iIndex] = kvConfig.GetFloat("damage"); RocketClassDamageIncrement[iIndex] = kvConfig.GetFloat("damage increment"); @@ -426,30 +420,20 @@ void ParseClasses(KeyValues kvConfig) iFlags |= RocketFlag_IsTRLimited; } - RocketClassElevationRate[iIndex] = kvConfig.GetFloat("elevation rate"); - RocketClassElevationLimit[iIndex] = kvConfig.GetFloat("elevation limit"); - RocketClassControlDelay[iIndex] = kvConfig.GetFloat("control delay"); - RocketClassMaxBounces[iIndex] = kvConfig.GetNum("max bounces"); - RocketClassBounceScale[iIndex] = kvConfig.GetFloat("bounce scale", 1.0); - RocketClassPlayerModifier[iIndex] = kvConfig.GetFloat("no. players modifier"); - RocketClassRocketsModifier[iIndex] = kvConfig.GetFloat("no. rockets modifier"); - RocketClassTargetWeight[iIndex] = kvConfig.GetFloat("direction to target weight"); - - // Scaling mode: orbit coefficient - RocketClassOrbitTightness[iIndex] = kvConfig.GetFloat("orbit tightness", 0.0); - - // Scaling mode: target speed - RocketClassMaxSpeed[iIndex] = kvConfig.GetFloat("max speed", 0.0); - RocketClassMaxDeflections[iIndex] = kvConfig.GetNum("max deflections", 0); - if (UseTargetSpeedScaling && RocketClassMaxDeflections[iIndex] > 0) - { - RocketClassSpeedIncrement[iIndex] = (RocketClassMaxSpeed[iIndex] - RocketClassSpeed[iIndex]) / float(RocketClassMaxDeflections[iIndex]); - } - - // Scaling mode: bounce vertical scale - RocketClassBounceVerticalScale[iIndex] = kvConfig.GetFloat("bounce vertical ratio", 1.0); - RocketClassBounceMaxVerticalSpeed[iIndex] = kvConfig.GetFloat("bounce max vertical speed", 0.0); - RocketClassDragPauseDuration[iIndex] = kvConfig.GetFloat("drag pause duration", 0.1); + RocketClassElevationRate[iIndex] = kvConfig.GetFloat("elevation rate"); + RocketClassElevationLimit[iIndex] = kvConfig.GetFloat("elevation limit"); + RocketClassControlDelay[iIndex] = kvConfig.GetFloat("control delay"); + RocketClassDragTimeMin[iIndex] = kvConfig.GetFloat("drag time min"); + RocketClassDragTimeMax[iIndex] = kvConfig.GetFloat("drag time max"); + RocketClassMaxBounces[iIndex] = kvConfig.GetNum("max bounces"); + RocketClassBounceScale[iIndex] = kvConfig.GetFloat("bounce scale", 1.0); + RocketClassBounceForceScale[iIndex] = kvConfig.GetFloat("bounce force scale", CvarBounceForceScale.FloatValue); + RocketClassBounceForceAngle[iIndex] = kvConfig.GetFloat("bounce force angle", CvarBounceForceAngle.FloatValue); + RocketClassCrawlBounceScale[iIndex] = kvConfig.GetFloat("crawl bounce scale", 0.4); + RocketClassCrawlBounceMaxUp[iIndex] = kvConfig.GetFloat("crawl bounce max up", 250.0); + RocketClassPlayerModifier[iIndex] = kvConfig.GetFloat("no. players modifier"); + RocketClassRocketsModifier[iIndex] = kvConfig.GetFloat("no. rockets modifier"); + RocketClassTargetWeight[iIndex] = kvConfig.GetFloat("direction to target weight"); DataPack hCmds = null; kvConfig.GetString("on spawn", strBuffer, sizeof(strBuffer)); @@ -510,31 +494,6 @@ void ParseSpawners(KeyValues kvConfig) SpawnersTrie.GetValue("blu", DefaultBluSpawner); } -/** Parses the "presets" section of the config (gameplay preset definitions). */ -void ParsePresets(KeyValues kvConfig) -{ - kvConfig.GotoFirstSubKey(); - do - { - int iIndex = PresetCount; - - kvConfig.GetString("name", PresetName[iIndex], sizeof(PresetName[])); - kvConfig.GetString("rocket class", PresetRocketClass[iIndex], sizeof(PresetRocketClass[])); - PresetMaxRockets[iIndex] = kvConfig.GetNum("max rockets", 1); - PresetSpawnInterval[iIndex] = kvConfig.GetFloat("spawn interval", 0.0); - - PresetCount++; - - if (PresetCount >= MAX_PRESETS) - { - LogError("Reached maximum presets (%d). Remaining presets will be ignored.", MAX_PRESETS); - break; - } - } - while (kvConfig.GotoNextKey()); - kvConfig.GoBack(); -} - /** * Parses a semicolon-separated command string into a DataPack. * Each command between semicolons becomes a separate entry. @@ -578,4 +537,4 @@ DataPack ParseCommands(char[] strLine) delete hCommands; return hDataPack; -} \ No newline at end of file +} diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc index db75e923..6fa0f5d2 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_core.inc @@ -9,6 +9,8 @@ // Tracks whether game events have been hooked (to prevent double-unhook errors) static bool EventsHooked = false; +// Tracks whether object_deflected was successfully hooked. +static bool ObjectDeflectedHooked = false; /** Checks if the current map is a dodgeball map (starts with "tfdb_", "db_", or "dbs_"). */ bool IsDodgeBallMap() @@ -33,7 +35,6 @@ void EnableDodgeBall() char mapFile[PLATFORM_MAX_PATH]; FormatEx(mapFile, sizeof(mapFile), "%s.cfg", mapName); ParseConfigurations(); - ParseConfigurations("presets.cfg"); ParseConfigurations(mapFile); if (RocketClassCount == 0) SetFailState("No rocket class defined."); @@ -52,7 +53,16 @@ void EnableDodgeBall() HookEventEx("player_death", OnPlayerDeath, EventHookMode_Pre); HookEventEx("post_inventory_application", OnPlayerInventory, EventHookMode_Post); HookEventEx("teamplay_broadcast_audio", OnBroadcastAudio, EventHookMode_Pre); - HookEventEx("object_deflected", OnObjectDeflected); + // Keep this hook global (not tied to enable/disable) to avoid + // map-end unhook exceptions observed on some installs. + if (!ObjectDeflectedHooked) + { + ObjectDeflectedHooked = HookEventEx("object_deflected", OnObjectDeflected); + if (!ObjectDeflectedHooked) + { + LogError("Failed to hook game event \"object_deflected\". Deflect-specific rocket updates will be disabled."); + } + } EventsHooked = true; } @@ -108,7 +118,6 @@ void DisableDodgeBall() DestroyRockets(); DestroyRocketClasses(); DestroySpawners(); - PresetCount = 0; if (LogicTimer != null) KillTimer(LogicTimer); LogicTimer = null; @@ -129,7 +138,6 @@ void DisableDodgeBall() UnhookEvent("player_death", OnPlayerDeath, EventHookMode_Pre); UnhookEvent("post_inventory_application", OnPlayerInventory, EventHookMode_Post); UnhookEvent("teamplay_broadcast_audio", OnBroadcastAudio, EventHookMode_Pre); - UnhookEvent("object_deflected", OnObjectDeflected); EventsHooked = false; } @@ -145,7 +153,7 @@ void DisableDodgeBall() } /** - * This is the main logic timer for the gamemode. + * This is the main 20Hz timer for the gamemode. * It handles rocket spawning and the logic for "legacy homing" rockets. * It also calls shared logic functions like sound updates. */ diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc index aa066c71..df39bd6c 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_events.inc @@ -136,7 +136,7 @@ public void OnPlayerInventory(Event hEvent, char[] strEventName, bool bDontBroad for (int iSlot = 1; iSlot < 5; iSlot++) { int iEntity = GetPlayerWeaponSlot(iClient, iSlot); - if (iEntity != -1) RemoveEntity(iEntity); + if (iEntity != -1) RemoveEdict(iEntity); } } @@ -200,11 +200,11 @@ public Action OnBroadcastAudio(Event hEvent, char[] strEventName, bool bDontBroa } /** Called when a projectile is deflected. Updates deflection count and drag state. */ -public void OnObjectDeflected(Event hEvent, char[] strEventName, bool bDontBroadcast) +public Action OnObjectDeflected(Event hEvent, const char[] strEventName, bool bDontBroadcast) { int iEntity = hEvent.GetInt("object_entindex"); int iIndex = FindRocketByEntity(iEntity); - if (iIndex == -1) return; + if (iIndex == -1) return Plugin_Continue; RocketEventDeflections[iIndex]++; @@ -229,12 +229,14 @@ public void OnObjectDeflected(Event hEvent, char[] strEventName, bool bDontBroad { SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); } + + return Plugin_Continue; } /** * This hook runs every server frame. * It's responsible for the smooth "homing" behaviour. - * "legacy homing" is handled by the logic timer OnDodgeBallGameFrame (~10Hz). + * "legacy homing" is handled by the 20Hz timer OnDodgeBallGameFrame. */ public void OnGameFrame() { @@ -350,27 +352,46 @@ public Action OnTouch(int iEntity, int iOther) float vBounceVec[3]; SubtractVectors(vVelocity, vNormal, vBounceVec); - ScaleVector(vBounceVec, RocketClassBounceScale[iClass]); - - // Z-clamp: scale vertical component to prevent extreme vertical bounces - if (UseBounceVerticalScale && RocketClassBounceVerticalScale[iClass] < 1.0) + int iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + if (!(RocketInstanceFlags[iIndex] & RocketFlag_CrawlBounce)) { - vBounceVec[2] *= RocketClassBounceVerticalScale[iClass]; - } - - // Absolute vertical speed cap (like UDL's crawl bounce max up) - if (UseBounceVerticalScale && RocketClassBounceMaxVerticalSpeed[iClass] > 0.0) - { - if (vBounceVec[2] > RocketClassBounceMaxVerticalSpeed[iClass]) + if (IsValidClient(iOwner, true)) + { + float fOwnerAngles[3]; + GetClientEyeAngles(iOwner, fOwnerAngles); + + if (fOwnerAngles[0] >= RocketClassBounceForceAngle[iClass]) + ScaleVector(vBounceVec, RocketClassBounceScale[iClass] * RocketClassBounceForceScale[iClass]); + else + ScaleVector(vBounceVec, RocketClassBounceScale[iClass]); + } + else { - vBounceVec[2] = RocketClassBounceMaxVerticalSpeed[iClass]; + ScaleVector(vBounceVec, RocketClassBounceScale[iClass]); } - else if (vBounceVec[2] < -RocketClassBounceMaxVerticalSpeed[iClass]) + } + else + { + float fBounceScale = RocketClassBounceScale[iClass]; + + if (IsValidClient(iOwner, true)) { - vBounceVec[2] = -RocketClassBounceMaxVerticalSpeed[iClass]; + float fOwnerAngles[3]; + GetClientEyeAngles(iOwner, fOwnerAngles); + if (fOwnerAngles[0] >= RocketClassBounceForceAngle[iClass]) + fBounceScale *= RocketClassBounceForceScale[iClass]; } + + // Crawl mode: preserve horizontal carry, suppress/clamp upward pop. + vBounceVec[0] *= fBounceScale; + vBounceVec[1] *= fBounceScale; + vBounceVec[2] *= fBounceScale * RocketClassCrawlBounceScale[iClass]; + + if (RocketClassCrawlBounceMaxUp[iClass] > 0.0 && vBounceVec[2] > RocketClassCrawlBounceMaxUp[iClass]) + vBounceVec[2] = RocketClassCrawlBounceMaxUp[iClass]; } + float vNewAngles[3]; GetVectorAngles(vBounceVec, vNewAngles); @@ -390,11 +411,6 @@ public Action OnTouch(int iEntity, int iOther) CopyVectors(vBounceVecRef, vBounceVec); } - // Pause homing — rocket flies freely with bounce velocity until - // the logic timer re-enables it (bounce pauses use timer, not per-frame drag). - RocketHomingPaused[iIndex] = true; - RocketIsDragPause[iIndex] = false; - TeleportEntity(iEntity, NULL_VECTOR, vNewAngles, vBounceVec); RocketBounces[iIndex]++; if (RocketInstanceFlags[iIndex] & RocketFlag_NoBounceDrags) diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc index 80b19f38..e9dede24 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_natives.inc @@ -304,6 +304,23 @@ public any Native_SetRocketClassControlDelay(Handle hPlugin, int iNumParams) { return 0; } +public any Native_GetRocketClassDragTimeMin(Handle hPlugin, int iNumParams) { + return view_as(RocketClassDragTimeMin[GetNativeCell(1)]); +} + +public any Native_SetRocketClassDragTimeMin(Handle hPlugin, int iNumParams) { + RocketClassDragTimeMin[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} + +public any Native_GetRocketClassDragTimeMax(Handle hPlugin, int iNumParams) { + return view_as(RocketClassDragTimeMax[GetNativeCell(1)]); +} + +public any Native_SetRocketClassDragTimeMax(Handle hPlugin, int iNumParams) { + RocketClassDragTimeMax[GetNativeCell(1)] = GetNativeCell(2); + return 0; +} public any Native_GetRocketClassMaxBounces(Handle hPlugin, int iNumParams) { return RocketClassMaxBounces[GetNativeCell(1)]; @@ -752,59 +769,4 @@ public any Native_HomingRocketThink(Handle hPlugin, int iNumParams) { public any Native_RocketLegacyThink(Handle hPlugin, int iNumParams) { RocketLegacyThink(GetNativeCell(1)); return 0; -} - -// ********************************************************************************* -// NATIVES - PRESETS -// ********************************************************************************* - -public any Native_GetPresetCount(Handle hPlugin, int iNumParams) { - return PresetCount; -} - -public any Native_GetPresetName(Handle hPlugin, int iNumParams) { - int iPreset = GetNativeCell(1); - int iMaxLen = GetNativeCell(3); - SetNativeString(2, PresetName[iPreset], iMaxLen); - return 0; -} - -public any Native_ApplyPreset(Handle hPlugin, int iNumParams) { - int iPreset = GetNativeCell(1); - if (iPreset < 0 || iPreset >= PresetCount) return false; - - // Find the rocket class index by short name - int iTargetClass = -1; - for (int i = 0; i < RocketClassCount; i++) - { - if (StrEqual(RocketClassName[i], PresetRocketClass[iPreset], false)) - { - iTargetClass = i; - break; - } - } - if (iTargetClass == -1) return false; - - // Destroy active rockets before changing settings - DestroyRockets(); - - // Apply to all spawners - for (int i = 0; i < SpawnersCount; i++) - { - SpawnersMaxRockets[i] = PresetMaxRockets[iPreset]; - - if (PresetSpawnInterval[iPreset] > 0.0) - { - SpawnersInterval[i] = PresetSpawnInterval[iPreset]; - } - - // Set chances table: 100% target class, 0% everything else - ArrayList hTable = SpawnersChancesTable[i]; - for (int j = 0; j < hTable.Length; j++) - { - hTable.Set(j, (j == iTargetClass) ? 100 : 0); - } - } - - return true; } \ No newline at end of file diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc index ca08f4a4..66df8cb7 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/dodgeball_rockets.inc @@ -68,9 +68,6 @@ void CreateRocket(int iSpawnerEntity, int iSpawnerClass, int iTeam, int iClass = RocketDeflections[iIndex] = 0; RocketEventDeflections[iIndex] = 0; RocketBounces[iIndex] = 0; - RocketHomingPaused[iIndex] = false; - RocketIsDragPause[iIndex] = false; - RocketDragPauseEnd[iIndex] = 0.0; RocketLastDeflectionTime[iIndex] = GetGameTime(); RocketLastBeepTime[iIndex] = GetGameTime(); RocketSpeed[iIndex] = CalculateRocketSpeed(iClass, fModifier); @@ -78,7 +75,7 @@ void CreateRocket(int iSpawnerEntity, int iSpawnerClass, int iTeam, int iClass = Internal_SetRocketState(iIndex, RocketState_None); CopyVectors(fDirection, RocketDirection[iIndex]); - SetEntDataFloat(iEntity, DamageOffset, CalculateRocketDamage(iClass, fModifier), true); + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, CalculateRocketDamage(iClass, fModifier), true); DispatchSpawn(iEntity); if (TestFlags(iFlags, RocketFlag_CustomModel)) @@ -198,101 +195,10 @@ int FindRocketByEntity(int iEntity) return -1; } -/** - * Processes a rocket deflection: reads the deflector's aim, selects a new target, - * recalculates speed/damage, checks for steals, and fires forwards/commands. - * Shared by HomingRocketThink, SharedRocketThink, and RocketLegacyThink. - * - * @param iIndex Rocket internal index. - * @param iEntity Rocket entity index. - * @param iClass Rocket class index. - * @param iFlags Rocket instance flags. - * @param iTeam Rocket's current team. - * @param iTargetTeam Team to select new target from. - * @param iDeflectionCount Current event deflection count. - * @param fModifier Speed/damage modifier for current deflection count. - * @param bClearDragging True to clear RocketState_Dragging (homing/shared). - * False for legacy (never enters drag state). - * @return False if Forward_OnRocketDeflectPre blocked the deflection. - */ -bool ProcessDeflection(int iIndex, int iEntity, int iClass, RocketFlags iFlags, - int iTeam, int iTargetTeam, int iDeflectionCount, float fModifier, - bool bClearDragging) -{ - int iClient = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); - - if (bClearDragging) - Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~(RocketState_Dragging | RocketState_Stolen))); - else - Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~RocketState_Stolen)); - - if (iClient >= 1) - { - float fViewAngles[3], fDirection[3]; - GetClientEyeAngles(iClient, fViewAngles); - GetAngleVectors(fViewAngles, fDirection, NULL_VECTOR, NULL_VECTOR); - CopyVectors(fDirection, RocketDirection[iIndex]); - UpdateRocketSkin(iEntity, iTeam, TestFlags(iFlags, RocketFlag_IsNeutral)); - - if (!(iFlags & RocketFlag_CanBeStolen)) - { - CheckStolenRocket(iClient, iIndex); - } - } - - int iTarget = SelectTarget(iTargetTeam, iIndex); - RocketTarget[iIndex] = EntIndexToEntRef(iTarget); - RocketDeflections[iIndex] = iDeflectionCount; - RocketLastDeflectionTime[iIndex] = GetGameTime(); - RocketSpeed[iIndex] = CalculateRocketSpeed(iClass, fModifier); - SetEntDataFloat(iEntity, DamageOffset, CalculateRocketDamage(iClass, fModifier), true); - - if (TestFlags(iFlags, RocketFlag_ElevateOnDeflect)) RocketInstanceFlags[iIndex] |= RocketFlag_Elevating; - - if ((iFlags & RocketFlag_IsSpeedLimited) && (RocketSpeed[iIndex] >= RocketClassSpeedLimit[iClass])) - { - RocketSpeed[iIndex] = RocketClassSpeedLimit[iClass]; - } - - RocketMphSpeed[iIndex] = RocketSpeed[iIndex] * HAMMER_TO_MPH; - - if (CvarStealPreventionDamage.BoolValue && (RocketInstanceState[iIndex] & RocketState_Stolen)) - { - SetEntDataFloat(iEntity, DamageOffset, 0.0, true); - } - - if (iFlags & RocketFlag_TeamlessHits) - { - SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); - } - - int iTargetRef = iTarget; - Action aResult = Forward_OnRocketDeflectPre(iIndex, iEntity, iClient, iTargetRef); - - if (aResult == Plugin_Stop || aResult == Plugin_Handled) - { - return false; - } - else if (aResult == Plugin_Changed) - { - iTarget = iTargetRef; - RocketTarget[iIndex] = EntIndexToEntRef(iTarget); - } - - EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); - if (TestFlags(iFlags, RocketFlag_OnDeflectCmd)) - { - ExecuteCommands(RocketClassCmdsOnDeflect[iClass], iClass, iEntity, iClient, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); - } - Forward_OnRocketDeflect(iIndex, iEntity, iClient); - - return true; -} - /** * Main think function for homing rockets. Handles tracking, dragging, * deflection, target switching, and all per-frame rocket behaviour. - * Called from the logic timer in dodgeball_core.inc (~10Hz due to SourceMod's 0.1s min timer). + * Called from the 20Hz timer in dodgeball_core.inc. * * @param iIndex The rocket's internal index. */ @@ -307,112 +213,144 @@ void HomingRocketThink(int iIndex) int iDeflectionCount = RocketEventDeflections[iIndex]; float fModifier = CalculateModifier(iClass, iDeflectionCount); - // --- Deflection detected: pause homing, defer aim reading to per-frame check --- if ((iDeflectionCount > RocketDeflections[iIndex]) && !(RocketInstanceState[iIndex] & RocketState_Dragging)) { - RocketHomingPaused[iIndex] = true; - RocketIsDragPause[iIndex] = true; - - // Snap drag pause to tick boundary so it ends on a deterministic frame. - // GetTickInterval() is the canonical frame duration from SM 1.12 (timers.inc). - float tickInterval = GetTickInterval(); - int pauseTicks = RoundToNearest(RocketClassDragPauseDuration[iClass] / tickInterval); - RocketDragPauseEnd[iIndex] = GetGameTime() + (pauseTicks * tickInterval); - + int iClient = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] | (RocketState_CanDrag | RocketState_Dragging))); - } - - // --- Drag pause expired: process deflection per-frame (allows sub-100ms drag) --- - if (RocketHomingPaused[iIndex] && RocketIsDragPause[iIndex] && GetGameTime() >= RocketDragPauseEnd[iIndex]) - { - if (iDeflectionCount > RocketDeflections[iIndex]) + if (iClient >= 1) { - if (!ProcessDeflection(iIndex, iEntity, iClass, iFlags, iTeam, iTargetTeam, iDeflectionCount, fModifier, true)) - { - RocketHomingPaused[iIndex] = false; - RocketIsDragPause[iIndex] = false; - return; - } + float fViewAngles[3], fDirection[3]; + GetClientEyeAngles(iClient, fViewAngles); + GetAngleVectors(fViewAngles, fDirection, NULL_VECTOR, NULL_VECTOR); + CopyVectors(fDirection, RocketDirection[iIndex]); + UpdateRocketSkin(iEntity, iTeam, TestFlags(iFlags, RocketFlag_IsNeutral)); } - - // Apply parameters and re-enable smooth homing - ApplyRocketParameters(iIndex); - RocketHomingPaused[iIndex] = false; - RocketIsDragPause[iIndex] = false; + RocketLastDeflectionTime[iIndex] = GetGameTime(); } - - // --- Normal homing: lerp toward target (only when not paused) --- - if (!RocketHomingPaused[iIndex]) + else { - if (!IsValidClient(iTarget, true)) + if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) <= RocketClassDragTimeMax[iClass] + GetTickInterval()) { - int iOwner = iTarget; - iTarget = SelectTarget(iTargetTeam, iIndex); - - if (!IsValidClient(iTarget, true)) return; - RocketTarget[iIndex] = EntIndexToEntRef(iTarget); - EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); - - if (TestFlags(iFlags, RocketFlag_OnNoTargetCmd)) + if ((RocketClassDragTimeMin[iClass] <= (GetGameTime() - RocketLastDeflectionTime[iIndex])) && (RocketInstanceState[iIndex] & RocketState_CanDrag)) { - ExecuteCommands(RocketClassCmdsOnNoTarget[iClass], iClass, iEntity, iOwner, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); - } - - if (CvarNoTargetRedirectDamage.BoolValue) - { - SetEntDataFloat(iEntity, DamageOffset, 0.0, true); + int iClient = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + if (iClient >= 1) + { + float fViewAngles[3], fDirection[3]; + GetClientEyeAngles(iClient, fViewAngles); + GetAngleVectors(fViewAngles, fDirection, NULL_VECTOR, NULL_VECTOR); + CopyVectors(fDirection, RocketDirection[iIndex]); + } } - Forward_OnRocketNoTarget(iIndex, iTarget, iOwner); } else { - if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) + if (!IsValidClient(iTarget, true)) { - // Scale turn rate from "per TICK_BASE_INTERVAL" to "per frame" using GetTickInterval(). - float fTickScale = GetTickInterval() / TICK_BASE_INTERVAL; - float fTurnrate = CalculateRocketTurnRate(iClass, fModifier) * fTickScale; - float fDirectionToTarget[3]; CalculateDirectionToClient(iEntity, iTarget, fDirectionToTarget); + int iOwner = iTarget; + iTarget = SelectTarget(iTargetTeam, iIndex); + + if (!IsValidClient(iTarget, true)) return; + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); - if (RocketInstanceFlags[iIndex] & RocketFlag_Elevating) + if (TestFlags(iFlags, RocketFlag_OnNoTargetCmd)) { - fDirectionToTarget[2] = RocketDirection[iIndex][2]; + ExecuteCommands(RocketClassCmdsOnNoTarget[iClass], iClass, iEntity, iOwner, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); } - if ((RocketInstanceFlags[iIndex] & RocketFlag_IsTRLimited) && (fTurnrate >= RocketClassTurnRateLimit[iClass] * fTickScale)) + if (CvarNoTargetRedirectDamage.BoolValue) { - fTurnrate = RocketClassTurnRateLimit[iClass] * fTickScale; + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, 0.0, true); } - LerpVectors(RocketDirection[iIndex], fDirectionToTarget, RocketDirection[iIndex], fTurnrate); + Forward_OnRocketNoTarget(iIndex, iTarget, iOwner); } - } - - // --- Smooth elevation (OnGameFrame, frame-rate independent) --- - if (UseSmoothElevation && (RocketInstanceFlags[iIndex] & RocketFlag_Elevating)) - { - if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) + else if ((iDeflectionCount > RocketDeflections[iIndex])) { - float fElevRate = RocketClassElevationRate[iClass] * (GetTickInterval() / TICK_BASE_INTERVAL); - if (RocketDirection[iIndex][2] < RocketClassElevationLimit[iClass]) + int iClient = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~(RocketState_Dragging | RocketState_Stolen))); + + if ((iClient >= 1) && !(iFlags & RocketFlag_CanBeStolen)) { - RocketDirection[iIndex][2] = FMin(RocketDirection[iIndex][2] + fElevRate, RocketClassElevationLimit[iClass]); + CheckStolenRocket(iClient, iIndex); } - else + + iTarget = SelectTarget(iTargetTeam, iIndex); + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + RocketDeflections[iIndex] = iDeflectionCount; + RocketSpeed[iIndex] = CalculateRocketSpeed(iClass, fModifier); + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, CalculateRocketDamage(iClass, fModifier), true); + + if (TestFlags(iFlags, RocketFlag_ElevateOnDeflect)) RocketInstanceFlags[iIndex] |= RocketFlag_Elevating; + + if ((iFlags & RocketFlag_IsSpeedLimited) && (RocketSpeed[iIndex] >= RocketClassSpeedLimit[iClass])) { - RocketInstanceFlags[iIndex] &= ~RocketFlag_Elevating; + RocketSpeed[iIndex] = RocketClassSpeedLimit[iClass]; + } + + RocketMphSpeed[iIndex] = RocketSpeed[iIndex] * HAMMER_TO_MPH; + + if (CvarStealPreventionDamage.BoolValue && (RocketInstanceState[iIndex] & RocketState_Stolen)) + { + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, 0.0, true); + } + + if (iFlags & RocketFlag_TeamlessHits) + { + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); + } + + int iTargetRef = iTarget; + Action aResult = Forward_OnRocketDeflectPre(iIndex, iEntity, iClient, iTargetRef); + + if (aResult == Plugin_Stop || aResult == Plugin_Handled) + { + return; + } + else if (aResult == Plugin_Changed) + { + iTarget = iTargetRef; + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + } + + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); + if (TestFlags(iFlags, RocketFlag_OnDeflectCmd)) + { + ExecuteCommands(RocketClassCmdsOnDeflect[iClass], iClass, iEntity, iClient, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); + } + Forward_OnRocketDeflect(iIndex, iEntity, iClient); + } + else + { + if ((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) + { + float fTurnrate = CalculateRocketTurnRate(iClass, fModifier) / TickModifier; + float fDirectionToTarget[3]; CalculateDirectionToClient(iEntity, iTarget, fDirectionToTarget); + + if (RocketInstanceFlags[iIndex] & RocketFlag_Elevating) + { + fDirectionToTarget[2] = RocketDirection[iIndex][2]; + } + + if ((RocketInstanceFlags[iIndex] & RocketFlag_IsTRLimited) && (fTurnrate >= RocketClassTurnRateLimit[iClass] / TickModifier)) + { + fTurnrate = RocketClassTurnRateLimit[iClass] / TickModifier; + } + LerpVectors(RocketDirection[iIndex], fDirectionToTarget, RocketDirection[iIndex], fTurnrate); } } } + } - if (!(RocketInstanceState[iIndex] & RocketState_Bouncing)) - { - ApplyRocketParameters(iIndex); - } + if (!(RocketInstanceState[iIndex] & RocketState_Bouncing)) + { + ApplyRocketParameters(iIndex); } } /** * Handles logic that is shared between all rocket behaviours, like sounds and delay checks. - * This is called from the logic timer in dodgeball_core.inc + * This is called from the 20Hz timer in dodgeball_core.inc */ void SharedRocketThink(int iIndex) { @@ -420,35 +358,12 @@ void SharedRocketThink(int iIndex) int iClass = RocketClass[iIndex]; RocketFlags iFlags = RocketInstanceFlags[iIndex]; int iTarget = EntRefToEntIndex(RocketTarget[iIndex]); - int iTeam = GetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1); - int iTargetTeam = (TestFlags(iFlags, RocketFlag_IsNeutral)) ? 0 : GetAnalogueTeam(iTeam); int iDeflectionCount = RocketEventDeflections[iIndex]; - float fModifier = CalculateModifier(iClass, iDeflectionCount); - // --- Handle paused state: bounce unpauses are handled here (timer-based) --- - // Drag unpauses are handled per-frame in HomingRocketThink for sub-100ms precision. - if (RocketHomingPaused[iIndex] && !RocketIsDragPause[iIndex]) - { - // Process pending deflection on the logic timer (bounce unpause) - if (iDeflectionCount > RocketDeflections[iIndex]) - { - if (!ProcessDeflection(iIndex, iEntity, iClass, iFlags, iTeam, iTargetTeam, iDeflectionCount, fModifier, true)) - { - RocketHomingPaused[iIndex] = false; - return; - } - } - - // Apply parameters once on unpause and re-enable smooth homing - ApplyRocketParameters(iIndex); - RocketHomingPaused[iIndex] = false; - } - - // --- Elevation (runs at logic timer rate when smooth elevation is off) --- if (!(iDeflectionCount > RocketDeflections[iIndex])) { if (((GetGameTime() - RocketLastDeflectionTime[iIndex]) >= RocketClassControlDelay[iClass]) && - (RocketInstanceFlags[iIndex] & RocketFlag_Elevating) && !UseSmoothElevation) + (RocketInstanceFlags[iIndex] & RocketFlag_Elevating)) { if (RocketDirection[iIndex][2] < RocketClassElevationLimit[iClass]) { @@ -477,7 +392,7 @@ void SharedRocketThink(int iIndex) /** * Think function for legacy homing rockets. Uses instant direction changes - * instead of smooth tracking. Called from the logic timer. + * instead of smooth tracking. Called from the 20Hz timer. * * @param iIndex The rocket's internal index. */ @@ -509,16 +424,74 @@ void RocketLegacyThink(int iIndex) if (CvarNoTargetRedirectDamage.BoolValue) { - SetEntDataFloat(iEntity, DamageOffset, 0.0, true); + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, 0.0, true); } Forward_OnRocketNoTarget(iIndex, iTarget, iOwner); } - else if (iDeflectionCount > RocketDeflections[iIndex]) + else if ((iDeflectionCount > RocketDeflections[iIndex])) { - if (!ProcessDeflection(iIndex, iEntity, iClass, iFlags, iTeam, iTargetTeam, iDeflectionCount, fModifier, false)) + int iClient = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity"); + Internal_SetRocketState(iIndex, (RocketInstanceState[iIndex] & ~RocketState_Stolen)); + + if (iClient >= 1) + { + float fViewAngles[3], fDirection[3]; + GetClientEyeAngles(iClient, fViewAngles); + GetAngleVectors(fViewAngles, fDirection, NULL_VECTOR, NULL_VECTOR); + CopyVectors(fDirection, RocketDirection[iIndex]); + UpdateRocketSkin(iEntity, iTeam, TestFlags(iFlags, RocketFlag_IsNeutral)); + + if (!(iFlags & RocketFlag_CanBeStolen)) + { + CheckStolenRocket(iClient, iIndex); + } + } + + iTarget = SelectTarget(iTargetTeam, iIndex); + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + RocketDeflections[iIndex] = iDeflectionCount; + RocketLastDeflectionTime[iIndex] = GetGameTime(); + RocketSpeed[iIndex] = CalculateRocketSpeed(iClass, fModifier); + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, CalculateRocketDamage(iClass, fModifier), true); + + if (TestFlags(iFlags, RocketFlag_ElevateOnDeflect)) RocketInstanceFlags[iIndex] |= RocketFlag_Elevating; + + if ((iFlags & RocketFlag_IsSpeedLimited) && (RocketSpeed[iIndex] >= RocketClassSpeedLimit[iClass])) + { + RocketSpeed[iIndex] = RocketClassSpeedLimit[iClass]; + } + + RocketMphSpeed[iIndex] = RocketSpeed[iIndex] * HAMMER_TO_MPH; + + if (CvarStealPreventionDamage.BoolValue && (RocketInstanceState[iIndex] & RocketState_Stolen)) + { + SetEntDataFloat(iEntity, FindSendPropInfo("CTFProjectile_Rocket", "m_iDeflected") + 4, 0.0, true); + } + + if (iFlags & RocketFlag_TeamlessHits) + { + SetEntProp(iEntity, Prop_Send, "m_iTeamNum", 1, 1); + } + + int iTargetRef = iTarget; + Action aResult = Forward_OnRocketDeflectPre(iIndex, iEntity, iClient, iTargetRef); + + if (aResult == Plugin_Stop || aResult == Plugin_Handled) { return; } + else if (aResult == Plugin_Changed) + { + iTarget = iTargetRef; + RocketTarget[iIndex] = EntIndexToEntRef(iTarget); + } + + EmitRocketSound(RocketSound_Alert, iClass, iEntity, iTarget, iFlags); + if (TestFlags(iFlags, RocketFlag_OnDeflectCmd)) + { + ExecuteCommands(RocketClassCmdsOnDeflect[iClass], iClass, iEntity, iClient, iTarget, LastDeadClient, RocketSpeed[iIndex], iDeflectionCount, RocketMphSpeed[iIndex]); + } + Forward_OnRocketDeflect(iIndex, iEntity, iClient); } else { @@ -574,12 +547,6 @@ float CalculateRocketSpeed(int iClass, float fModifier) /** Calculates turn rate based on class settings and modifier. */ float CalculateRocketTurnRate(int iClass, float fModifier) { - if (UseOrbitCoefficient && RocketClassOrbitTightness[iClass] > 0.0) - { - // Scale by 0.001 so config values use a friendly 0.1–1.0 range - // (0.3 ≈ default turn rate feel, 0.1 = loose, 1.0 = tight) - return CalculateRocketSpeed(iClass, fModifier) * RocketClassOrbitTightness[iClass] * 0.001; - } return RocketClassTurnRate[iClass] + RocketClassTurnRateIncrement[iClass] * fModifier; } diff --git a/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc b/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc index 5e56ce1e..a6717d4e 100644 --- a/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc +++ b/roles/sourcemod/files/addons/sourcemod/scripting/include/tfdb.inc @@ -4,30 +4,9 @@ #define _tfdb_included // ---- General settings ----------------------------------------------------------- -// -// Timer rate vs OnGameFrame: -// SourceMod's TimerSys.cpp defines TIMER_MIN_ACCURACY = 0.1 (seconds). -// The timer processing loop (RunFrame) only fires every ~0.1s regardless of -// the interval passed to CreateTimer — so the logic timer runs at ~10Hz. -// The actual fire interval is NOT exactly 0.1s: it depends on tickrate since -// 0.1s must be rounded up to the next tick boundary (e.g. 7 ticks on 66-tick -// = 0.1061s, 13 ticks on 128-tick = 0.1016s). -// -// OnGameFrame fires every server tick (~66Hz on 66-tick, ~128Hz on 128-tick). -// We use it for smooth per-frame homing. Turn rate and elevation values in the -// config are calibrated as "amount per 0.1s" for backwards compatibility. -// Per-frame scaling uses GetTickInterval() directly — no derived magic numbers. -// -#define FPS_LOGIC_RATE 10.0 +#define FPS_LOGIC_RATE 20.0 #define FPS_LOGIC_INTERVAL 1.0 / FPS_LOGIC_RATE -/** - * The base time interval (seconds) that config turn rate / elevation values - * are calibrated against. Turn rate 0.05 means "lerp 0.05 per 0.1s." - * Per-frame code divides by this and multiplies by GetTickInterval(). - */ -#define TICK_BASE_INTERVAL 0.1 - /** Conversion factor: Hammer units/s to MPH. (1 HU ≈ 0.01905m) */ #define HAMMER_TO_MPH 0.042614 @@ -36,7 +15,6 @@ #define MAX_ROCKET_CLASSES 50 #define MAX_SPAWNER_CLASSES 50 #define MAX_SPAWN_POINTS 100 -#define MAX_PRESETS 16 // ---- Flags and types constants -------------------------------------------------- @@ -81,8 +59,9 @@ enum RocketFlags RocketFlag_NoBounceDrags = 1 << 21, /**< Can't drag while bouncing */ RocketFlag_OnNoTargetCmd = 1 << 22, /**< Execute commands when no target */ RocketFlag_CanBeStolen = 1 << 23, /**< Rocket can be stolen by other players */ - RocketFlag_StealTeamCheck = 1 << 24 /**< Only teammates can steal */ -}; + RocketFlag_StealTeamCheck = 1 << 24, /**< Only teammates can steal */ + RocketFlag_CrawlBounce = 1 << 25 /**< Use crawl bounce scaling/clamping */ + }; /** * Sound types for rocket audio. @@ -464,6 +443,17 @@ native float TFDB_GetRocketClassControlDelay(int iClass); /** Sets the control delay. */ native void TFDB_SetRocketClassControlDelay(int iClass, float fDelay); +/** Gets the minimum drag time. */ +native float TFDB_GetRocketClassDragTimeMin(int iClass); + +/** Sets the minimum drag time. */ +native void TFDB_SetRocketClassDragTimeMin(int iClass, float fMin); + +/** Gets the maximum drag time. */ +native float TFDB_GetRocketClassDragTimeMax(int iClass); + +/** Sets the maximum drag time. */ +native void TFDB_SetRocketClassDragTimeMax(int iClass, float fMax); /** Sets the total number of rocket classes. */ native void TFDB_SetRocketClassCount(int iCount); @@ -497,10 +487,7 @@ native float TFDB_GetSpawnersInterval(int iSpawnerClass); /** Sets the spawn interval (seconds between rockets). */ native void TFDB_SetSpawnersInterval(int iSpawnerClass, float fInterval); -/** - * Gets the rocket class spawn chances table. - * @note You MUST call delete on the returned ArrayList Handle when finished, or your plugin will leak memory. - */ +/** Gets the rocket class spawn chances table. */ native ArrayList TFDB_GetSpawnersChancesTable(int iSpawnerClass); /** Sets the rocket class spawn chances table. */ @@ -690,46 +677,31 @@ native void TFDB_SetRocketClassTargetWeight(int iClass, float fWeight); // ============ Command Handlers ============ -/** - * Gets the commands to execute on rocket spawn. - * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. - */ +/** Gets the commands to execute on rocket spawn. */ native DataPack TFDB_GetRocketClassCmdsOnSpawn(int iClass); /** Sets the commands to execute on rocket spawn. */ native void TFDB_SetRocketClassCmdsOnSpawn(int iClass, DataPack hCmds); -/** - * Gets the commands to execute on deflection. - * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. - */ +/** Gets the commands to execute on deflection. */ native DataPack TFDB_GetRocketClassCmdsOnDeflect(int iClass); /** Sets the commands to execute on deflection. */ native void TFDB_SetRocketClassCmdsOnDeflect(int iClass, DataPack hCmds); -/** - * Gets the commands to execute when rocket kills a player. - * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. - */ +/** Gets the commands to execute when rocket kills a player. */ native DataPack TFDB_GetRocketClassCmdsOnKill(int iClass); /** Sets the commands to execute when rocket kills a player. */ native void TFDB_SetRocketClassCmdsOnKill(int iClass, DataPack hCmds); -/** - * Gets the commands to execute when rocket explodes. - * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. - */ +/** Gets the commands to execute when rocket explodes. */ native DataPack TFDB_GetRocketClassCmdsOnExplode(int iClass); /** Sets the commands to execute when rocket explodes. */ native void TFDB_SetRocketClassCmdsOnExplode(int iClass, DataPack hCmds); -/** - * Gets the commands to execute when rocket has no target. - * @note You MUST call delete on the returned DataPack Handle when finished, or your plugin will leak memory. - */ +/** Gets the commands to execute when rocket has no target. */ native DataPack TFDB_GetRocketClassCmdsOnNoTarget(int iClass); /** Sets the commands to execute when rocket has no target. */ @@ -805,33 +777,6 @@ native void TFDB_GetStealInfo(int iClient, bool &stole, int &count); */ native void TFDB_SetStealInfo(int iClient, bool stole, int count); -// ============ Preset Functions ============ - -/** - * Gets the number of loaded presets. - * - * @return Number of presets (0 if none loaded or presets.cfg missing). - */ -native int TFDB_GetPresetCount(); - -/** - * Gets the display name of a preset. - * - * @param iPreset Preset index (0 to TFDB_GetPresetCount()-1). - * @param buffer Buffer to store the name. - * @param maxlen Maximum buffer length. - */ -native void TFDB_GetPresetName(int iPreset, char[] buffer, int maxlen); - -/** - * Applies a preset: destroys active rockets, sets spawner max rockets, - * spawn interval, and chances table to 100% the preset's rocket class. - * - * @param iPreset Preset index (0 to TFDB_GetPresetCount()-1). - * @return True if applied successfully, false if invalid preset or class not found. - */ -native bool TFDB_ApplyPreset(int iPreset); - public SharedPlugin __pl_TFDB = { name = "tfdb", @@ -892,6 +837,10 @@ public void __pl_TFDB_SetNTVOptional() MarkNativeAsOptional("TFDB_SetRocketClassPlayerModifier"); MarkNativeAsOptional("TFDB_GetRocketClassControlDelay"); MarkNativeAsOptional("TFDB_SetRocketClassControlDelay"); + MarkNativeAsOptional("TFDB_GetRocketClassDragTimeMin"); + MarkNativeAsOptional("TFDB_SetRocketClassDragTimeMin"); + MarkNativeAsOptional("TFDB_GetRocketClassDragTimeMax"); + MarkNativeAsOptional("TFDB_SetRocketClassDragTimeMax"); MarkNativeAsOptional("TFDB_SetRocketClassCount"); MarkNativeAsOptional("TFDB_SetRocketEntity"); MarkNativeAsOptional("TFDB_GetRocketClassMaxBounces"); @@ -987,8 +936,5 @@ public void __pl_TFDB_SetNTVOptional() MarkNativeAsOptional("TFDB_SetRocketState"); MarkNativeAsOptional("TFDB_GetStealInfo"); MarkNativeAsOptional("TFDB_SetStealInfo"); - MarkNativeAsOptional("TFDB_GetPresetCount"); - MarkNativeAsOptional("TFDB_GetPresetName"); - MarkNativeAsOptional("TFDB_ApplyPreset"); } -#endif \ No newline at end of file +#endif diff --git a/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt b/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt index a17dbad3..85fa3bb2 100644 --- a/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt +++ b/roles/sourcemod/files/addons/sourcemod/translations/tfdb.phrases.txt @@ -5,11 +5,6 @@ "en" "[{olive}TFDB{default}] This command is disabled." } - "Command_NoAccess" - { - "en" "[{olive}TFDB{default}] You do not have permission to use this command." - } - "Command_DBExplosion_Usage" { "en" "Usage : tf_dodgeball_explosion " @@ -624,21 +619,4 @@ "#format" "{1:i}" "en" "[{olive}TFDB{default}] The rockets count has been changed to {community}{1}{default}." } - - "Dodgeball_PresetVote_Cooldown" - { - "#format" "{1:i}" - "en" "[{olive}TFDB{default}] Voting for presets is in cooldown for {darkorange}{1}{default} seconds." - } - - "Dodgeball_PresetVote_NoPresets" - { - "en" "[{olive}TFDB{default}] No presets are configured." - } - - "Dodgeball_PresetVote_Applied" - { - "#format" "{1:s}" - "en" "[{olive}TFDB{default}] Preset changed to {community}{1}{default}." - } } diff --git a/roles/srcds/tasks/main.yml b/roles/srcds/tasks/main.yml index fe277a18..a15b7146 100644 --- a/roles/srcds/tasks/main.yml +++ b/roles/srcds/tasks/main.yml @@ -599,13 +599,13 @@ tags: - game_engine -- name: "Trigger srcds reload" - ansible.builtin.debug: - msg: "Reloading srcds services: {{ srcds_services | join(', ') }}" - notify: "Restart SRCDS" - when: srcds_services is defined and srcds_services | length > 0 - tags: - - game_engine +# - name: "Trigger srcds reload" +# ansible.builtin.debug: +# msg: "Reloading srcds services: {{ srcds_services | join(', ') }}" +# notify: "Restart SRCDS" +# when: srcds_services is defined and srcds_services | length > 0 +# tags: +# - game_engine - name: Cron srcds become: true diff --git a/roles/srcds/templates/dodgeball_general.cfg.j2 b/roles/srcds/templates/dodgeball_general.cfg.j2 index eb1854fe..181d1ceb 100644 --- a/roles/srcds/templates/dodgeball_general.cfg.j2 +++ b/roles/srcds/templates/dodgeball_general.cfg.j2 @@ -102,7 +102,7 @@ // >>> Basic parameters <<< "name" "Homing Rocket" // Full name of the rocket type "behaviour" "homing" // "homing" for smooth, per-frame updates. - // "legacy homing" for classic, logic timer updates (~10Hz). + // "legacy homing" for classic, 20Hz timer updates. "model" "" // Default: Common rocket model "is animated" "0" // Only works when using a custom model (Default : 0) @@ -162,57 +162,31 @@ "speed" "875" // Base speed for the rocket. "speed increment" "160" // Speed increment per reflection. "speed limit" "0" // Speed limit for the rocket (0 to disable speed limiting) - //"max speed" "3500" // Target max speed (used when "target speed scaling" = 1) - //"max deflections" "40" // Deflections to reach max speed (used when "target speed scaling" = 1) - "turn rate" "0.260" // Turn rate / tick for this rocket (base orbit radius). - // Community sweet spot is often 0.180-0.320 combined with increment. - "turn rate increment" "0.0180" // Increment per reflection. At TF2's max speed cap (~3500 HU/s), - // continuous turn rates of 1.8-3.2 provide the best orbit feel. + "turn rate" "0.260" // Turn rate / tick for this rocket. + "turn rate increment" "0.0180" // Increment per reflection. "turn rate limit" "0" // Maximum turn rate when deflected (0 to disable turn rate limiting) // Cannot exceed 1.0 regardless of the actual limit value - //"orbit tightness" "0.3" // Constant orbit coefficient (used when "orbit coefficient" = 1) - // 0.3 matches classic turn rate feel. 0.1 = loose, 1.0 = tight. "elevation rate" "0" // Elevation rate when deflected (if enabled) "elevation limit" "0" // Maximum elevation when deflected (if enabled) "control delay" "0" // Delay until the rocket starts tracking the target after a deflection. + // "drag time min": After a deflect, this is the delay (in seconds) before the player's aim starts guiding the rocket. + // "drag time max": This is the total duration (in seconds) the player can guide the rocket after the initial min_time delay. + // Example: min 0.1, max 0.5 means dragging starts 0.1s after deflect and lasts for 0.5s, ending at 0.6s after deflect. + // This does not affect "legacy homing" rockets. + "drag time min" "0.05" + "drag time max" "0.05" + "max bounces" "1000" // How many times can this rocket bounce? "bounce scale" "1.0" // How hard should the rocket bounce? (Default : 1.0 multiplier) - "bounce force scale" "1.5" - "bounce force angle" "45.0" - //"bounce vertical ratio" "1.0" // Scale vertical bounce velocity. 1.0 = full bounce, 0.5 = half height, - // 0.0 = no vertical bounce at all. Lower = flatter bounces. - // (used when "bounce vertical scale" = 1) - //"bounce max vertical speed" "250" // Hard cap on vertical bounce speed in HU/s. No matter how fast - // the rocket is, it can't bounce higher than this. 0 = no cap. - // (used when "bounce vertical scale" = 1) - + "bounce force scale" "1.5" // Multiplier for how much stronger a player-forced bounce is. + "bounce force angle" "45.0" // Minimum downward angle a player must look to trigger a stronger bounce. "crawl bounce" "1" // Use crawl bounce scaling/clamping. "crawl bounce scale" "1" // Extra Z scaling in crawl mode. - "crawl bounce max up" "1500" - - "no. players modifier" "0" // Increment based upon the number of players in the server. - "no. rockets modifier" "0" // Increment based upon the number of rockets fired since the start of the round. - "direction to target weight" "100" // Weight modifier for target selection, based upon the direction of the rocket - // to the client. - // Drag pause: how long the rocket flies straight after a deflect - // before reading the player's aim and resuming homing. - // This is the "drag window" — the brief moment where the rocket - // goes forward and your mouse determines its initial trajectory. - // - // 0.1 = legacy feel (100ms, matches DB_Reborn / default timer rate) - // 0.05 = snappier response (50ms, rocket snaps to aim faster) - // 0.0 = instant (no forward flight, aim read immediately) - // 0.15 = sluggish (150ms, more time to aim but slower feel) - // - // Uses per-frame timing (OnGameFrame, fires every server tick). - // Snapped to tick boundaries internally: e.g. 0.1s on 66-tick - // = 7 ticks (0.106s), on 128-tick = 13 ticks (0.102s). - // - "drag pause duration" "0.05" + "crawl bounce max up" "1500" // Max upward velocity after bounce in crawl mode (0 disables cap). "critical chance" "100" // Percentage of chance for a critical rocket. @@ -224,8 +198,7 @@ // >>> Events <<< "on spawn" "" // Actions to execute on rocket spawn. "on deflect" "" // Actions to execute when a rocket is deflected. - "on kill" "" - // "on kill" "tf_dodgeball_print [{olive}TFDB{default}] {yellow}★ KILL{default} | {lightblue}##@owner## {default}killed {red}##@dead## {default}| Deflects: {darkorange}@deflections {default}| Speed: {red}@capmphspeed mph" // Actions to execute when a rocket kills a client. + "on kill" "" // Actions to execute when a rocket kills a client. "on explode" "" // Actions to execute when a rocket kills a client (triggered once). "on no target" "" // Actions to execute when a rocket has an invalid target. "on destroyed" "" // Actions to execute when a rocket explodes (will not trigger if the player is killed by the rocket). diff --git a/sm_plugins/TF2-Dodgeball-Modified b/sm_plugins/TF2-Dodgeball-Modified deleted file mode 160000 index effc5875..00000000 --- a/sm_plugins/TF2-Dodgeball-Modified +++ /dev/null @@ -1 +0,0 @@ -Subproject commit effc5875cfac501413f68974e7167241d81e8536 diff --git a/sm_plugins/TF2-Dodgeball-Modified-UDL b/sm_plugins/TF2-Dodgeball-Modified-UDL new file mode 160000 index 00000000..b3b21818 --- /dev/null +++ b/sm_plugins/TF2-Dodgeball-Modified-UDL @@ -0,0 +1 @@ +Subproject commit b3b21818dd615fde6cad12f61df7fdca243d9cf5 diff --git a/sm_plugins_update.sh b/sm_plugins_update.sh index f9db69d2..c870366e 100755 --- a/sm_plugins_update.sh +++ b/sm_plugins_update.sh @@ -3,8 +3,8 @@ ROOT=$(pwd) SM_ROOT=$ROOT/roles/sourcemod/files/addons/sourcemod SRC_ROOT="sm_plugins" -DODGEBALL_ROOT="$SRC_ROOT/TF2-Dodgeball-Modified" -DODGEBALL_BRANCH="v2.1.0" +DODGEBALL_ROOT="$SRC_ROOT/TF2-Dodgeball-Modified-UDL" +DODGEBALL_BRANCH="master" STAC_ROOT="$SRC_ROOT/stac" STAC_BRANCH="gbans-native" @@ -67,7 +67,7 @@ cp -rv Subplugins/Print/scripting/tfdb_print.sp $SM_ROOT/scripting/ #cp -rv TF2Dodgeball/cfg/sourcemod/* $ROOT/roles/sourcemod/files/cfg/sourcemod/ popd || exit - +exit pushd $HALLOWEENCOSMETICS_ROOT || exit git fetch --all git checkout $HALLOWEENCOSMETICS_BRANCH From 2f18fab9efaa970511dea94c67e617c8d2a0a5b2 Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Tue, 3 Mar 2026 21:34:32 -0700 Subject: [PATCH 5/6] revert --- roles/srcds/tasks/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/roles/srcds/tasks/main.yml b/roles/srcds/tasks/main.yml index a15b7146..fe277a18 100644 --- a/roles/srcds/tasks/main.yml +++ b/roles/srcds/tasks/main.yml @@ -599,13 +599,13 @@ tags: - game_engine -# - name: "Trigger srcds reload" -# ansible.builtin.debug: -# msg: "Reloading srcds services: {{ srcds_services | join(', ') }}" -# notify: "Restart SRCDS" -# when: srcds_services is defined and srcds_services | length > 0 -# tags: -# - game_engine +- name: "Trigger srcds reload" + ansible.builtin.debug: + msg: "Reloading srcds services: {{ srcds_services | join(', ') }}" + notify: "Restart SRCDS" + when: srcds_services is defined and srcds_services | length > 0 + tags: + - game_engine - name: Cron srcds become: true From 09049b436f837813317967d8a82bf3b2b9f5e78c Mon Sep 17 00:00:00 2001 From: Leigh MacDonald Date: Tue, 3 Mar 2026 21:41:21 -0700 Subject: [PATCH 6/6] Update workflow --- .github/workflows/build.yml | 14 +++++++------- roles/srcds/tasks/main.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b21bdbcf..13591b75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Ansible Lint # feel free to pick your own name +name: Ansible Lint # feel free to pick your own name on: [push, pull_request] @@ -7,10 +7,10 @@ jobs: name: Ansible Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run ansible-lint uses: ansible/ansible-lint@main -# # optional (see below): + # # optional (see below): with: args: "--exclude sm_plugins watcher" setup_python: "true" @@ -21,12 +21,12 @@ jobs: name: Build Plugins runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup SourcePawn Compiler uses: rumblefrog/setup-sp@master with: - version: "1.12.x" + version: "1.13.x" - name: Ensure plugin builds working-directory: ./roles/sourcemod/files/addons/sourcemod/scripting @@ -36,7 +36,7 @@ jobs: echo -e "\nCompiling $file... to ../plugins/${f}.smx" spcomp -w234 -O2 -v2 -i include $file -o ../plugins/$f.smx done - + echo "===OUT FILES===" ls - echo version = ${{ steps.setup_sp.outputs.plugin-version }} \ No newline at end of file + echo version = ${{ steps.setup_sp.outputs.plugin-version }} diff --git a/roles/srcds/tasks/main.yml b/roles/srcds/tasks/main.yml index fe277a18..131e98df 100644 --- a/roles/srcds/tasks/main.yml +++ b/roles/srcds/tasks/main.yml @@ -271,7 +271,7 @@ loop_control: index_var: loop0 -- name: mkdir tf/addons/sourcemod/configs/dodgeball +- name: Make tf/addons/sourcemod/configs/dodgeball ansible.builtin.file: path: ~/srcds-{{ item.server_name_short }}/tf/addons/sourcemod/configs/dodgeball state: directory