summaryrefslogtreecommitdiff
path: root/sourcemod/scripting/gokz-core
diff options
context:
space:
mode:
authornavewindre <nw@moneybot.cc>2023-12-04 18:06:10 +0100
committernavewindre <nw@moneybot.cc>2023-12-04 18:06:10 +0100
commitaef0d1c1268ab7d4bc18996c9c6b4da16a40aadc (patch)
tree43e766b51704f4ab8b383583bdc1871eeeb9c698 /sourcemod/scripting/gokz-core
parent38f1140c11724da05a23a10385061200b907cf6e (diff)
bbbbbbbbwaaaaaaaaaaa
Diffstat (limited to 'sourcemod/scripting/gokz-core')
-rw-r--r--sourcemod/scripting/gokz-core/commands.sp385
-rw-r--r--sourcemod/scripting/gokz-core/demofix.sp110
-rw-r--r--sourcemod/scripting/gokz-core/forwards.sp401
-rw-r--r--sourcemod/scripting/gokz-core/map/buttons.sp138
-rw-r--r--sourcemod/scripting/gokz-core/map/end.sp155
-rw-r--r--sourcemod/scripting/gokz-core/map/mapfile.sp502
-rw-r--r--sourcemod/scripting/gokz-core/map/prefix.sp48
-rw-r--r--sourcemod/scripting/gokz-core/map/starts.sp219
-rw-r--r--sourcemod/scripting/gokz-core/map/triggers.sp855
-rw-r--r--sourcemod/scripting/gokz-core/map/zones.sp183
-rw-r--r--sourcemod/scripting/gokz-core/menus/mode_menu.sp40
-rw-r--r--sourcemod/scripting/gokz-core/menus/options_menu.sp174
-rw-r--r--sourcemod/scripting/gokz-core/misc.sp803
-rw-r--r--sourcemod/scripting/gokz-core/modes.sp106
-rw-r--r--sourcemod/scripting/gokz-core/natives.sp647
-rw-r--r--sourcemod/scripting/gokz-core/options.sp438
-rw-r--r--sourcemod/scripting/gokz-core/teamnumfix.sp68
-rw-r--r--sourcemod/scripting/gokz-core/teleports.sp917
-rw-r--r--sourcemod/scripting/gokz-core/timer/pause.sp257
-rw-r--r--sourcemod/scripting/gokz-core/timer/timer.sp368
-rw-r--r--sourcemod/scripting/gokz-core/timer/virtual_buttons.sp322
-rw-r--r--sourcemod/scripting/gokz-core/triggerfix.sp622
22 files changed, 7758 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-core/commands.sp b/sourcemod/scripting/gokz-core/commands.sp
new file mode 100644
index 0000000..6aba82c
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/commands.sp
@@ -0,0 +1,385 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_options", CommandOptions, "[KZ] Open the options menu.");
+ RegConsoleCmd("sm_o", CommandOptions, "[KZ] Open the options menu.");
+ RegConsoleCmd("sm_checkpoint", CommandMakeCheckpoint, "[KZ] Set a checkpoint.");
+ RegConsoleCmd("sm_gocheck", CommandTeleportToCheckpoint, "[KZ] Teleport to your current checkpoint.");
+ RegConsoleCmd("sm_prev", CommandPrevCheckpoint, "[KZ] Go back a checkpoint.");
+ RegConsoleCmd("sm_next", CommandNextCheckpoint, "[KZ] Go forward a checkpoint.");
+ RegConsoleCmd("sm_undo", CommandUndoTeleport, "[KZ] Undo teleport.");
+ RegConsoleCmd("sm_start", CommandTeleportToStart, "[KZ] Teleport to the start.");
+ RegConsoleCmd("sm_searchstart", CommandSearchStart, "[KZ] Teleport to the start zone/button of a specified course.");
+ RegConsoleCmd("sm_end", CommandTeleportToEnd, "[KZ] Teleport to the end.");
+ RegConsoleCmd("sm_restart", CommandTeleportToStart, "[KZ] Teleport to your start position.");
+ RegConsoleCmd("sm_r", CommandTeleportToStart, "[KZ] Teleport to your start position.");
+ RegConsoleCmd("sm_setstartpos", CommandSetStartPos, "[KZ] Set your custom start position to your current position.");
+ RegConsoleCmd("sm_ssp", CommandSetStartPos, "[KZ] Set your custom start position to your current position.");
+ RegConsoleCmd("sm_clearstartpos", CommandClearStartPos, "[KZ] Clear your custom start position.");
+ RegConsoleCmd("sm_csp", CommandClearStartPos, "[KZ] Clear your custom start position.");
+ RegConsoleCmd("sm_main", CommandMain, "[KZ] Teleport to the start of the main course.");
+ RegConsoleCmd("sm_m", CommandMain, "[KZ] Teleport to the start of the main course.");
+ RegConsoleCmd("sm_bonus", CommandBonus, "[KZ] Teleport to the start of a bonus. Usage: `!bonus <#bonus>");
+ RegConsoleCmd("sm_b", CommandBonus, "[KZ] Teleport to the start of a bonus. Usage: `!b <#bonus>");
+ RegConsoleCmd("sm_pause", CommandTogglePause, "[KZ] Toggle pausing your timer and stopping you in your position.");
+ RegConsoleCmd("sm_resume", CommandTogglePause, "[KZ] Toggle pausing your timer and stopping you in your position.");
+ RegConsoleCmd("sm_stop", CommandStopTimer, "[KZ] Stop your timer.");
+ RegConsoleCmd("sm_virtualbuttonindicators", CommandToggleVirtualButtonIndicators, "[KZ] Toggle virtual button indicators.");
+ RegConsoleCmd("sm_vbi", CommandToggleVirtualButtonIndicators, "[KZ] Toggle virtual button indicators.");
+ RegConsoleCmd("sm_virtualbuttons", CommandToggleVirtualButtonsLock, "[KZ] Toggle locking virtual buttons, preventing them from being moved.");
+ RegConsoleCmd("sm_vb", CommandToggleVirtualButtonsLock, "[KZ] Toggle locking virtual buttons, preventing them from being moved.");
+ RegConsoleCmd("sm_mode", CommandMode, "[KZ] Open the movement mode selection menu.");
+ RegConsoleCmd("sm_vanilla", CommandVanilla, "[KZ] Switch to the Vanilla mode.");
+ RegConsoleCmd("sm_vnl", CommandVanilla, "[KZ] Switch to the Vanilla mode.");
+ RegConsoleCmd("sm_v", CommandVanilla, "[KZ] Switch to the Vanilla mode.");
+ RegConsoleCmd("sm_simplekz", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode.");
+ RegConsoleCmd("sm_skz", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode.");
+ RegConsoleCmd("sm_s", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode.");
+ RegConsoleCmd("sm_kztimer", CommandKZTimer, "[KZ] Switch to the KZTimer mode.");
+ RegConsoleCmd("sm_kzt", CommandKZTimer, "[KZ] Switch to the KZTimer mode.");
+ RegConsoleCmd("sm_k", CommandKZTimer, "[KZ] Switch to the KZTimer mode.");
+ RegConsoleCmd("sm_nc", CommandToggleNoclip, "[KZ] Toggle noclip.");
+ RegConsoleCmd("+noclip", CommandEnableNoclip, "[KZ] Noclip on.");
+ RegConsoleCmd("-noclip", CommandDisableNoclip, "[KZ] Noclip off.");
+ RegConsoleCmd("sm_ncnt", CommandToggleNoclipNotrigger, "[KZ] Toggle noclip-notrigger.");
+ RegConsoleCmd("+noclipnt", CommandEnableNoclipNotrigger, "[KZ] Noclip-notrigger on.");
+ RegConsoleCmd("-noclipnt", CommandDisableNoclipNotrigger, "[KZ] Noclip-notrigger off.");
+ RegConsoleCmd("sm_sg", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard.");
+ RegConsoleCmd("sm_safe", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard.");
+ RegConsoleCmd("sm_safeguard", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard.");
+ RegConsoleCmd("sm_pro", CommandProSafeGuard, "[KZ] Toggle PRO safeguard.");
+ RegConsoleCmd("kill", CommandKill);
+ RegConsoleCmd("killvector", CommandKill);
+ RegConsoleCmd("explode", CommandKill);
+ RegConsoleCmd("explodevector", CommandKill);
+}
+
+void AddCommandsListeners()
+{
+ AddCommandListener(CommandJoinTeam, "jointeam");
+}
+
+bool SwitchToModeIfAvailable(int client, int mode)
+{
+ if (!GOKZ_GetModeLoaded(mode))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Mode Not Available", gC_ModeNames[mode]);
+ return false;
+ }
+ else
+ {
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+ GOKZ_SetCoreOption(client, Option_Mode, mode);
+ return true;
+ }
+}
+
+public Action CommandKill(int client, int args)
+{
+ if (IsPlayerAlive(client) && GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+public Action CommandOptions(int client, int args)
+{
+ DisplayOptionsMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandJoinTeam(int client, const char[] command, int argc)
+{
+ char teamString[4];
+ GetCmdArgString(teamString, sizeof(teamString));
+ int team = StringToInt(teamString);
+
+ if (team == CS_TEAM_SPECTATOR)
+ {
+ if (!GOKZ_GetPaused(client) && !GOKZ_GetCanPause(client))
+ {
+ SendFakeTeamEvent(client);
+ return Plugin_Handled;
+ }
+ }
+ else if (IsPlayerAlive(client) && GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ SendFakeTeamEvent(client);
+ return Plugin_Handled;
+ }
+ GOKZ_JoinTeam(client, team);
+ return Plugin_Handled;
+}
+
+public Action CommandMakeCheckpoint(int client, int args)
+{
+ GOKZ_MakeCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandTeleportToCheckpoint(int client, int args)
+{
+ GOKZ_TeleportToCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandPrevCheckpoint(int client, int args)
+{
+ GOKZ_PrevCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandNextCheckpoint(int client, int args)
+{
+ GOKZ_NextCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandUndoTeleport(int client, int args)
+{
+ GOKZ_UndoTeleport(client);
+ return Plugin_Handled;
+}
+
+public Action CommandTeleportToStart(int client, int args)
+{
+ GOKZ_TeleportToStart(client);
+ return Plugin_Handled;
+}
+
+public Action CommandSearchStart(int client, int args)
+{
+ if (args == 0)
+ {
+ GOKZ_TeleportToSearchStart(client, GetCurrentCourse(client));
+ return Plugin_Handled;
+ }
+ else
+ {
+ char argCourse[4];
+ GetCmdArg(1, argCourse, sizeof(argCourse));
+ int course = StringToInt(argCourse);
+ if (GOKZ_IsValidCourse(course, false))
+ {
+ GOKZ_TeleportToSearchStart(client, course);
+ }
+ else if (StrEqual(argCourse, "main", false) || course == 0)
+ {
+ GOKZ_TeleportToSearchStart(client, 0);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Course Number", argCourse);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandTeleportToEnd(int client, int args)
+{
+ if (args == 0)
+ {
+ GOKZ_TeleportToEnd(client, GetCurrentCourse(client));
+ }
+ else
+ {
+ char argCourse[4];
+ GetCmdArg(1, argCourse, sizeof(argCourse));
+ int course = StringToInt(argCourse);
+ if (GOKZ_IsValidCourse(course, false))
+ {
+ GOKZ_TeleportToEnd(client, course);
+ }
+ else if (StrEqual(argCourse, "main", false) || course == 0)
+ {
+ GOKZ_TeleportToEnd(client, 0);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Course Number", argCourse);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandSetStartPos(int client, int args)
+{
+ SetStartPositionToCurrent(client, StartPositionType_Custom);
+
+ GOKZ_PrintToChat(client, true, "%t", "Set Custom Start Position");
+ if (GOKZ_GetCoreOption(client, Option_CheckpointSounds) == CheckpointSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_CHECKPOINT, _, "Set Start Position");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action CommandClearStartPos(int client, int args)
+{
+ if (ClearCustomStartPosition(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Cleared Custom Start Position");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action CommandMain(int client, int args)
+{
+ TeleportToCourseStart(client, 0);
+ return Plugin_Handled;
+}
+
+public Action CommandBonus(int client, int args)
+{
+ if (args == 0)
+ { // Go to Bonus 1
+ TeleportToCourseStart(client, 1);
+ }
+ else
+ { // Go to specified Bonus #
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ TeleportToCourseStart(client, bonus);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandTogglePause(int client, int args)
+{
+ if (!IsPlayerAlive(client))
+ {
+ GOKZ_RespawnPlayer(client);
+ }
+ else
+ {
+ TogglePause(client);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandStopTimer(int client, int args)
+{
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped");
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleVirtualButtonIndicators(int client, int args)
+{
+ if (GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled)
+ {
+ GOKZ_SetCoreOption(client, Option_VirtualButtonIndicators, VirtualButtonIndicators_Enabled);
+ }
+ else
+ {
+ GOKZ_SetCoreOption(client, Option_VirtualButtonIndicators, VirtualButtonIndicators_Disabled);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleVirtualButtonsLock(int client, int args)
+{
+ if (ToggleVirtualButtonsLock(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Locked Virtual Buttons");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Unlocked Virtual Buttons");
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandMode(int client, int args)
+{
+ DisplayModeMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandVanilla(int client, int args)
+{
+ SwitchToModeIfAvailable(client, Mode_Vanilla);
+ return Plugin_Handled;
+}
+
+public Action CommandSimpleKZ(int client, int args)
+{
+ SwitchToModeIfAvailable(client, Mode_SimpleKZ);
+ return Plugin_Handled;
+}
+
+public Action CommandKZTimer(int client, int args)
+{
+ SwitchToModeIfAvailable(client, Mode_KZTimer);
+ return Plugin_Handled;
+}
+
+public Action CommandToggleNoclip(int client, int args)
+{
+ ToggleNoclip(client);
+ return Plugin_Handled;
+}
+
+public Action CommandEnableNoclip(int client, int args)
+{
+ EnableNoclip(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDisableNoclip(int client, int args)
+{
+ DisableNoclip(client);
+ return Plugin_Handled;
+}
+
+public Action CommandToggleNoclipNotrigger(int client, int args)
+{
+ ToggleNoclipNotrigger(client);
+ return Plugin_Handled;
+}
+
+public Action CommandEnableNoclipNotrigger(int client, int args)
+{
+ EnableNoclipNotrigger(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDisableNoclipNotrigger(int client, int args)
+{
+ DisableNoclipNotrigger(client);
+ return Plugin_Handled;
+}
+
+public Action CommandNubSafeGuard(int client, int args)
+{
+ ToggleNubSafeGuard(client);
+ return Plugin_Handled;
+}
+
+public Action CommandProSafeGuard(int client, int args)
+{
+ ToggleProSafeGuard(client);
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/demofix.sp b/sourcemod/scripting/gokz-core/demofix.sp
new file mode 100644
index 0000000..84a9307
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/demofix.sp
@@ -0,0 +1,110 @@
+static ConVar CV_EnableDemofix;
+static Handle H_DemofixTimer;
+static bool mapRunning;
+
+void OnPluginStart_Demofix()
+{
+ AddCommandListener(Command_Demorestart, "demorestart");
+ CV_EnableDemofix = AutoExecConfig_CreateConVar("gokz_demofix", "1", "Whether GOKZ applies demo record fix to server. (0 = Disabled, 1 = Update warmup period once, 2 = Regularly reset warmup period)", _, true, 0.0, true, 2.0);
+ CV_EnableDemofix.AddChangeHook(OnDemofixConVarChanged);
+ // If the map is tweaking the warmup value, we need to rerun the fix again.
+ FindConVar("mp_warmuptime").AddChangeHook(OnDemofixConVarChanged);
+ // We assume that the map is already loaded on late load.
+ if (gB_LateLoad)
+ {
+ mapRunning = true;
+ }
+}
+
+void OnMapStart_Demofix()
+{
+ mapRunning = true;
+}
+
+void OnMapEnd_Demofix()
+{
+ mapRunning = false;
+}
+
+void OnRoundStart_Demofix()
+{
+ DoDemoFix();
+}
+
+public Action Command_Demorestart(int client, const char[] command, int argc)
+{
+ FixRecord(client);
+ return Plugin_Continue;
+}
+
+static void FixRecord(int client)
+{
+ // For some reasons, demo playback speed is absolute trash without a round_start event.
+ // So whenever the client starts recording a demo, we create the event and send it to them.
+ Event e = CreateEvent("round_start", true);
+ int timelimit = FindConVar("mp_timelimit").IntValue;
+ e.SetInt("timelimit", timelimit);
+ e.SetInt("fraglimit", 0);
+ e.SetString("objective", "demofix");
+
+ e.FireToClient(client);
+ delete e;
+}
+
+public void OnDemofixConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ DoDemoFix();
+}
+
+public Action Timer_EnableDemoRecord(Handle timer)
+{
+ EnableDemoRecord();
+ return Plugin_Continue;
+}
+
+static void DoDemoFix()
+{
+ if (H_DemofixTimer != null)
+ {
+ delete H_DemofixTimer;
+ }
+ // Setting the cvar value to 1 can avoid clogging the demo file and slightly increase performance.
+ switch (CV_EnableDemofix.IntValue)
+ {
+ case 0:
+ {
+ if (!mapRunning)
+ {
+ return;
+ }
+
+ GameRules_SetProp("m_bWarmupPeriod", 0);
+ }
+ case 1:
+ {
+ // Set warmup time to 2^31-1, effectively forever
+ if (FindConVar("mp_warmuptime").IntValue != 2147483647)
+ {
+ FindConVar("mp_warmuptime").SetInt(2147483647);
+ }
+ EnableDemoRecord();
+ }
+ case 2:
+ {
+ H_DemofixTimer = CreateTimer(1.0, Timer_EnableDemoRecord, _, TIMER_REPEAT);
+ }
+ }
+}
+
+static void EnableDemoRecord()
+{
+ // Enable warmup to allow demo recording
+ // m_fWarmupPeriodEnd is set in the past to hide the timer UI
+ if (!mapRunning)
+ {
+ return;
+ }
+ GameRules_SetProp("m_bWarmupPeriod", 1);
+ GameRules_SetPropFloat("m_fWarmupPeriodStart", GetGameTime() - 1.0);
+ GameRules_SetPropFloat("m_fWarmupPeriodEnd", GetGameTime() - 1.0);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/forwards.sp b/sourcemod/scripting/gokz-core/forwards.sp
new file mode 100644
index 0000000..efb064f
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/forwards.sp
@@ -0,0 +1,401 @@
+static GlobalForward H_OnOptionsLoaded;
+static GlobalForward H_OnOptionChanged;
+static GlobalForward H_OnTimerStart;
+static GlobalForward H_OnTimerStart_Post;
+static GlobalForward H_OnTimerEnd;
+static GlobalForward H_OnTimerEnd_Post;
+static GlobalForward H_OnTimerEndMessage;
+static GlobalForward H_OnTimerStopped;
+static GlobalForward H_OnPause;
+static GlobalForward H_OnPause_Post;
+static GlobalForward H_OnResume;
+static GlobalForward H_OnResume_Post;
+static GlobalForward H_OnMakeCheckpoint;
+static GlobalForward H_OnMakeCheckpoint_Post;
+static GlobalForward H_OnTeleportToCheckpoint;
+static GlobalForward H_OnTeleportToCheckpoint_Post;
+static GlobalForward H_OnTeleport;
+static GlobalForward H_OnPrevCheckpoint;
+static GlobalForward H_OnPrevCheckpoint_Post;
+static GlobalForward H_OnNextCheckpoint;
+static GlobalForward H_OnNextCheckpoint_Post;
+static GlobalForward H_OnTeleportToStart;
+static GlobalForward H_OnTeleportToStart_Post;
+static GlobalForward H_OnTeleportToEnd;
+static GlobalForward H_OnTeleportToEnd_Post;
+static GlobalForward H_OnUndoTeleport;
+static GlobalForward H_OnUndoTeleport_Post;
+static GlobalForward H_OnCountedTeleport_Post;
+static GlobalForward H_OnStartPositionSet_Post;
+static GlobalForward H_OnJumpValidated;
+static GlobalForward H_OnJumpInvalidated;
+static GlobalForward H_OnJoinTeam;
+static GlobalForward H_OnFirstSpawn;
+static GlobalForward H_OnModeLoaded;
+static GlobalForward H_OnModeUnloaded;
+static GlobalForward H_OnTimerNativeCalledExternally;
+static GlobalForward H_OnOptionsMenuCreated;
+static GlobalForward H_OnOptionsMenuReady;
+static GlobalForward H_OnCourseRegistered;
+static GlobalForward H_OnRunInvalidated;
+static GlobalForward H_OnEmitSoundToClient;
+
+void CreateGlobalForwards()
+{
+ H_OnOptionsLoaded = new GlobalForward("GOKZ_OnOptionsLoaded", ET_Ignore, Param_Cell);
+ H_OnOptionChanged = new GlobalForward("GOKZ_OnOptionChanged", ET_Ignore, Param_Cell, Param_String, Param_Cell);
+ H_OnTimerStart = new GlobalForward("GOKZ_OnTimerStart", ET_Hook, Param_Cell, Param_Cell);
+ H_OnTimerStart_Post = new GlobalForward("GOKZ_OnTimerStart_Post", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnTimerEnd = new GlobalForward("GOKZ_OnTimerEnd", ET_Hook, Param_Cell, Param_Cell, Param_Float, Param_Cell);
+ H_OnTimerEnd_Post = new GlobalForward("GOKZ_OnTimerEnd_Post", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Cell);
+ H_OnTimerEndMessage = new GlobalForward("GOKZ_OnTimerEndMessage", ET_Hook, Param_Cell, Param_Cell, Param_Float, Param_Cell);
+ H_OnTimerStopped = new GlobalForward("GOKZ_OnTimerStopped", ET_Ignore, Param_Cell);
+ H_OnPause = new GlobalForward("GOKZ_OnPause", ET_Hook, Param_Cell);
+ H_OnPause_Post = new GlobalForward("GOKZ_OnPause_Post", ET_Ignore, Param_Cell);
+ H_OnResume = new GlobalForward("GOKZ_OnResume", ET_Hook, Param_Cell);
+ H_OnResume_Post = new GlobalForward("GOKZ_OnResume_Post", ET_Ignore, Param_Cell);
+ H_OnMakeCheckpoint = new GlobalForward("GOKZ_OnMakeCheckpoint", ET_Hook, Param_Cell);
+ H_OnMakeCheckpoint_Post = new GlobalForward("GOKZ_OnMakeCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnTeleportToCheckpoint = new GlobalForward("GOKZ_OnTeleportToCheckpoint", ET_Hook, Param_Cell);
+ H_OnTeleportToCheckpoint_Post = new GlobalForward("GOKZ_OnTeleportToCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnTeleport = new GlobalForward("GOKZ_OnTeleport", ET_Hook, Param_Cell);
+ H_OnPrevCheckpoint = new GlobalForward("GOKZ_OnPrevCheckpoint", ET_Hook, Param_Cell);
+ H_OnPrevCheckpoint_Post = new GlobalForward("GOKZ_OnPrevCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnNextCheckpoint = new GlobalForward("GOKZ_OnNextCheckpoint", ET_Hook, Param_Cell);
+ H_OnNextCheckpoint_Post = new GlobalForward("GOKZ_OnNextCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnTeleportToStart = new GlobalForward("GOKZ_OnTeleportToStart", ET_Hook, Param_Cell, Param_Cell);
+ H_OnTeleportToStart_Post = new GlobalForward("GOKZ_OnTeleportToStart_Post", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnTeleportToEnd = new GlobalForward("GOKZ_OnTeleportToEnd", ET_Hook, Param_Cell, Param_Cell);
+ H_OnTeleportToEnd_Post = new GlobalForward("GOKZ_OnTeleportToEnd_Post", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnUndoTeleport = new GlobalForward("GOKZ_OnUndoTeleport", ET_Hook, Param_Cell);
+ H_OnUndoTeleport_Post = new GlobalForward("GOKZ_OnUndoTeleport_Post", ET_Ignore, Param_Cell);
+ H_OnStartPositionSet_Post = new GlobalForward("GOKZ_OnStartPositionSet_Post", ET_Ignore, Param_Cell, Param_Cell, Param_Array, Param_Array);
+ H_OnCountedTeleport_Post = new GlobalForward("GOKZ_OnCountedTeleport_Post", ET_Ignore, Param_Cell);
+ H_OnJumpValidated = new GlobalForward("GOKZ_OnJumpValidated", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+ H_OnJumpInvalidated = new GlobalForward("GOKZ_OnJumpInvalidated", ET_Ignore, Param_Cell);
+ H_OnJoinTeam = new GlobalForward("GOKZ_OnJoinTeam", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnFirstSpawn = new GlobalForward("GOKZ_OnFirstSpawn", ET_Ignore, Param_Cell);
+ H_OnModeLoaded = new GlobalForward("GOKZ_OnModeLoaded", ET_Ignore, Param_Cell);
+ H_OnModeUnloaded = new GlobalForward("GOKZ_OnModeUnloaded", ET_Ignore, Param_Cell);
+ H_OnTimerNativeCalledExternally = new GlobalForward("GOKZ_OnTimerNativeCalledExternally", ET_Event, Param_Cell, Param_Cell);
+ H_OnOptionsMenuCreated = new GlobalForward("GOKZ_OnOptionsMenuCreated", ET_Ignore, Param_Cell);
+ H_OnOptionsMenuReady = new GlobalForward("GOKZ_OnOptionsMenuReady", ET_Ignore, Param_Cell);
+ H_OnCourseRegistered = new GlobalForward("GOKZ_OnCourseRegistered", ET_Ignore, Param_Cell);
+ H_OnRunInvalidated = new GlobalForward("GOKZ_OnRunInvalidated", ET_Ignore, Param_Cell);
+ H_OnEmitSoundToClient = new GlobalForward("GOKZ_OnEmitSoundToClient", ET_Hook, Param_Cell, Param_String, Param_FloatByRef, Param_String);
+}
+
+void Call_GOKZ_OnOptionsLoaded(int client)
+{
+ Call_StartForward(H_OnOptionsLoaded);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnOptionChanged(int client, const char[] option, int optionValue)
+{
+ Call_StartForward(H_OnOptionChanged);
+ Call_PushCell(client);
+ Call_PushString(option);
+ Call_PushCell(optionValue);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerStart(int client, int course, Action &result)
+{
+ Call_StartForward(H_OnTimerStart);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTimerStart_Post(int client, int course)
+{
+ Call_StartForward(H_OnTimerStart_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerEnd(int client, int course, float time, int teleportsUsed, Action &result)
+{
+ Call_StartForward(H_OnTimerEnd);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ Call_StartForward(H_OnTimerEnd_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed, Action &result)
+{
+ Call_StartForward(H_OnTimerEndMessage);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTimerStopped(int client)
+{
+ Call_StartForward(H_OnTimerStopped);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnPause(int client, Action &result)
+{
+ Call_StartForward(H_OnPause);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnPause_Post(int client)
+{
+ Call_StartForward(H_OnPause_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnResume(int client, Action &result)
+{
+ Call_StartForward(H_OnResume);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnResume_Post(int client)
+{
+ Call_StartForward(H_OnResume_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnMakeCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnMakeCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnMakeCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnMakeCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleportToCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnTeleportToCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTeleportToCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnTeleportToCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleport(int client)
+{
+ Call_StartForward(H_OnTeleport);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnPrevCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnPrevCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnPrevCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnPrevCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnNextCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnNextCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnNextCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnNextCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleportToStart(int client, int course, Action &result)
+{
+ Call_StartForward(H_OnTeleportToStart);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTeleportToStart_Post(int client, int course)
+{
+ Call_StartForward(H_OnTeleportToStart_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleportToEnd(int client, int course, Action &result)
+{
+ Call_StartForward(H_OnTeleportToEnd);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTeleportToEnd_Post(int client, int course)
+{
+ Call_StartForward(H_OnTeleportToEnd_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnUndoTeleport(int client, Action &result)
+{
+ Call_StartForward(H_OnUndoTeleport);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnUndoTeleport_Post(int client)
+{
+ Call_StartForward(H_OnUndoTeleport_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnCountedTeleport_Post(int client)
+{
+ Call_StartForward(H_OnCountedTeleport_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3])
+{
+ Call_StartForward(H_OnStartPositionSet_Post);
+ Call_PushCell(client);
+ Call_PushCell(type);
+ Call_PushArray(origin, 3);
+ Call_PushArray(angles, 3);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ Call_StartForward(H_OnJumpValidated);
+ Call_PushCell(client);
+ Call_PushCell(jumped);
+ Call_PushCell(ladderJump);
+ Call_PushCell(jumpbug);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnJumpInvalidated(int client)
+{
+ Call_StartForward(H_OnJumpInvalidated);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnJoinTeam(int client, int team)
+{
+ Call_StartForward(H_OnJoinTeam);
+ Call_PushCell(client);
+ Call_PushCell(team);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnFirstSpawn(int client)
+{
+ Call_StartForward(H_OnFirstSpawn);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnModeLoaded(int mode)
+{
+ Call_StartForward(H_OnModeLoaded);
+ Call_PushCell(mode);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnModeUnloaded(int mode)
+{
+ Call_StartForward(H_OnModeUnloaded);
+ Call_PushCell(mode);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client, Action &result)
+{
+ Call_StartForward(H_OnTimerNativeCalledExternally);
+ Call_PushCell(plugin);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ Call_StartForward(H_OnOptionsMenuCreated);
+ Call_PushCell(topMenu);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ Call_StartForward(H_OnOptionsMenuReady);
+ Call_PushCell(topMenu);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnCourseRegistered(int course)
+{
+ Call_StartForward(H_OnCourseRegistered);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnRunInvalidated(int client)
+{
+ Call_StartForward(H_OnRunInvalidated);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description, Action &result)
+{
+ Call_StartForward(H_OnEmitSoundToClient);
+ Call_PushCell(client);
+ Call_PushString(sample);
+ Call_PushFloatRef(volume);
+ Call_PushString(description);
+ Call_Finish(result);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/buttons.sp b/sourcemod/scripting/gokz-core/map/buttons.sp
new file mode 100644
index 0000000..8923fbd
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/buttons.sp
@@ -0,0 +1,138 @@
+/*
+ Hooks between specifically named func_buttons and GOKZ.
+*/
+
+
+
+static Regex RE_BonusStartButton;
+static Regex RE_BonusEndButton;
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapButtons()
+{
+ RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX);
+ RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX);
+}
+
+void OnEntitySpawned_MapButtons(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+ if (!StrEqual("func_button", buffer, false))
+ {
+ return;
+ }
+
+ if (GetEntityName(entity, buffer, sizeof(buffer)) == 0)
+ {
+ return;
+ }
+
+ int course = 0;
+ if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnStartButtonPress);
+ RegisterCourseStart(course);
+ }
+ else if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnEndButtonPress);
+ RegisterCourseEnd(course);
+ }
+ else if ((course = GetStartButtonBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnBonusStartButtonPress);
+ RegisterCourseStart(course);
+ }
+ else if ((course = GetEndButtonBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnBonusEndButtonPress);
+ RegisterCourseEnd(course);
+ }
+}
+
+public void OnStartButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessStartButtonPress(activator, 0);
+}
+
+public void OnEndButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessEndButtonPress(activator, 0);
+}
+
+public void OnBonusStartButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetStartButtonBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessStartButtonPress(activator, course);
+}
+
+public void OnBonusEndButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetEndButtonBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessEndButtonPress(activator, course);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void ProcessStartButtonPress(int client, int course)
+{
+ if (GOKZ_StartTimer(client, course))
+ {
+ // Only calling on success is intended behaviour (and prevents virtual button exploits)
+ OnStartButtonPress_Teleports(client, course);
+ OnStartButtonPress_VirtualButtons(client, course);
+ }
+}
+
+static void ProcessEndButtonPress(int client, int course)
+{
+ GOKZ_EndTimer(client, course);
+ OnEndButtonPress_VirtualButtons(client, course);
+}
+
+static int GetStartButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1);
+}
+
+static int GetEndButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/end.sp b/sourcemod/scripting/gokz-core/map/end.sp
new file mode 100644
index 0000000..d119084
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/end.sp
@@ -0,0 +1,155 @@
+/*
+ Hooks between specifically named end destinations and GOKZ
+*/
+
+
+
+static Regex RE_BonusEndButton;
+static Regex RE_BonusEndZone;
+static CourseTimerType endType[GOKZ_MAX_COURSES];
+static float endOrigin[GOKZ_MAX_COURSES][3];
+static float endAngles[GOKZ_MAX_COURSES][3];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapEnd()
+{
+ RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX);
+ RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX);
+}
+
+void OnEntitySpawnedPost_MapEnd(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+
+ if (StrEqual("trigger_multiple", buffer, false))
+ {
+ bool isEndZone;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false))
+ {
+ isEndZone = true;
+ StoreEnd(0, entity, CourseTimerType_ZoneNew);
+ }
+ else if (GetEndZoneBonusNumber(entity) != -1)
+ {
+ int course = GetEndZoneBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isEndZone = true;
+ StoreEnd(course, entity, CourseTimerType_ZoneNew);
+ }
+ }
+ }
+ if (!isEndZone)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer)
+ {
+ StoreEnd(trigger.course, entity, CourseTimerType_ZoneLegacy);
+ }
+ }
+ }
+ else if (StrEqual("func_button", buffer, false))
+ {
+ bool isEndButton;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false))
+ {
+ isEndButton = true;
+ StoreEnd(0, entity, CourseTimerType_Button);
+ }
+ else
+ {
+ int course = GetEndButtonBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isEndButton = true;
+ StoreEnd(course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+ if (!isEndButton)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer)
+ {
+ StoreEnd(trigger.course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+}
+
+void OnMapStart_MapEnd()
+{
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ endType[course] = CourseTimerType_None;
+ }
+}
+
+bool GetMapEndPosition(int course, float origin[3], float angles[3])
+{
+ if (endType[course] == CourseTimerType_None)
+ {
+ return false;
+ }
+
+ origin = endOrigin[course];
+ angles = endAngles[course];
+
+ return true;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void StoreEnd(int course, int entity, CourseTimerType type)
+{
+ // If StoreEnd is called, then there is at least an end position (even though it might not be a valid one)
+ if (endType[course] < CourseTimerType_Default)
+ {
+ endType[course] = CourseTimerType_Default;
+ }
+
+ // Real zone is always better than "fake" zones which are better than buttons
+ // as the buttons found in a map with fake zones aren't meant to be visible.
+ if (endType[course] >= type)
+ {
+ return;
+ }
+
+ float origin[3], distFromCenter[3];
+ GetEntityPositions(entity, origin, endOrigin[course], endAngles[course], distFromCenter);
+
+ // If it is a button or the center of the center of the zone is invalid
+ if (type == CourseTimerType_Button || !IsSpawnValid(endOrigin[course]))
+ {
+ // Attempt with various positions around the entity, pick the first valid one.
+ if (!FindValidPositionAroundTimerEntity(entity, endOrigin[course], endAngles[course], type == CourseTimerType_Button))
+ {
+ endOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone.
+ return;
+ }
+ }
+
+ // Only update the CourseTimerType if a valid position is found.
+ endType[course] = type;
+}
+
+static int GetEndButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1);
+}
+
+static int GetEndZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1);
+}
diff --git a/sourcemod/scripting/gokz-core/map/mapfile.sp b/sourcemod/scripting/gokz-core/map/mapfile.sp
new file mode 100644
index 0000000..db60e7e
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/mapfile.sp
@@ -0,0 +1,502 @@
+/*
+ Mapping API
+
+ Reads data from the current map file.
+*/
+
+static Regex RE_BonusStartButton;
+static Regex RE_BonusEndButton;
+
+// NOTE: 4 megabyte array for entity lump reading.
+static char gEntityLump[4194304];
+
+// =====[ PUBLIC ]=====
+
+void EntlumpParse(StringMap antiBhopTriggers, StringMap teleportTriggers, StringMap timerButtonTriggers, int &mappingApiVersion)
+{
+ char mapPath[512];
+ GetCurrentMap(mapPath, sizeof(mapPath));
+ Format(mapPath, sizeof(mapPath), "maps/%s.bsp", mapPath);
+
+ // https://developer.valvesoftware.com/wiki/Source_BSP_File_Format
+
+ File file = OpenFile(mapPath, "rb");
+ if (file != INVALID_HANDLE)
+ {
+ int identifier;
+ file.ReadInt32(identifier);
+
+ if (identifier == GOKZ_BSP_HEADER_IDENTIFIER)
+ {
+ // skip version number
+ file.Seek(4, SEEK_CUR);
+
+ // the entity lump info is the first lump in the array, so we don't need to seek any further.
+ int offset;
+ int length;
+ file.ReadInt32(offset);
+ file.ReadInt32(length);
+
+ // jump to the start of the entity lump
+ file.Seek(offset, SEEK_SET);
+
+ int charactersRead = file.ReadString(gEntityLump, sizeof(gEntityLump), length);
+ delete file;
+ if (charactersRead >= sizeof(gEntityLump) - 1)
+ {
+ PushMappingApiError("ERROR: Entity lump: The map's entity lump is too big! Reduce the amount of entities in your map.");
+ return;
+ }
+ gEntityLump[length] = '\0';
+
+ int index = 0;
+
+ StringMap entity = new StringMap();
+ bool gotWorldSpawn = false;
+ while (EntlumpParseEntity(entity, gEntityLump, index))
+ {
+ char classname[128];
+ char targetName[GOKZ_ENTLUMP_MAX_VALUE];
+ entity.GetString("classname", classname, sizeof(classname));
+
+ if (!gotWorldSpawn && StrEqual("worldspawn", classname, false))
+ {
+ gotWorldSpawn = true;
+ char versionString[32];
+ if (entity.GetString("climb_mapping_api_version", versionString, sizeof(versionString)))
+ {
+ if (StringToIntEx(versionString, mappingApiVersion) == 0)
+ {
+ PushMappingApiError("ERROR: Entity lump: Couldn't parse Mapping API version from map properties: \"%s\".", versionString);
+ mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+ }
+ }
+ else
+ {
+ // map doesn't have a mapping api version.
+ mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+ }
+ }
+ else if (StrEqual("trigger_multiple", classname, false))
+ {
+ TriggerType triggerType;
+ if (!gotWorldSpawn || mappingApiVersion != GOKZ_MAPPING_API_VERSION_NONE)
+ {
+ if (entity.GetString("targetname", targetName, sizeof(targetName)))
+ {
+ // get trigger properties if applicable
+ triggerType = GetTriggerType(targetName);
+ if (triggerType == TriggerType_Antibhop)
+ {
+ AntiBhopTrigger trigger;
+ if (GetAntiBhopTriggerEntityProperties(trigger, entity))
+ {
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ antiBhopTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ }
+ else if (triggerType == TriggerType_Teleport)
+ {
+ TeleportTrigger trigger;
+ if (GetTeleportTriggerEntityProperties(trigger, entity))
+ {
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ teleportTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ }
+ }
+ }
+
+ // Tracking legacy timer triggers that press the timer buttons upon triggered.
+ if (triggerType == TriggerType_Invalid)
+ {
+ char touchOutput[128];
+ ArrayList value;
+
+ if (entity.GetString("OnStartTouch", touchOutput, sizeof(touchOutput)))
+ {
+ TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers);
+ }
+ else if (entity.GetValue("OnStartTouch", value)) // If there are multiple outputs, we have to check for all of them.
+ {
+ for (int i = 0; i < value.Length; i++)
+ {
+ value.GetString(i, touchOutput, sizeof(touchOutput));
+ TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers);
+ }
+ }
+ }
+ }
+ else if (StrEqual("func_button", classname, false))
+ {
+ char pressOutput[128];
+ ArrayList value;
+
+ if (entity.GetString("OnPressed", pressOutput, sizeof(pressOutput)))
+ {
+ TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers);
+ }
+ else if (entity.GetValue("OnPressed", value)) // If there are multiple outputs, we have to check for all of them.
+ {
+ for (int i = 0; i < value.Length; i++)
+ {
+ value.GetString(i, pressOutput, sizeof(pressOutput));
+ TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers);
+ }
+ }
+ }
+ // clear for next loop
+ entity.Clear();
+ }
+ delete entity;
+ }
+ delete file;
+ }
+ else
+ {
+ // TODO: do something more elegant
+ SetFailState("Catastrophic extreme hyperfailure! Mapping API Couldn't open the map file for reading! %s. The map file might be gone or another program is using it.", mapPath);
+ }
+}
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapFile()
+{
+ char buffer[64];
+ char press[8];
+ FormatEx(press, sizeof(press), "%s%s", CHAR_ESCAPE, "Press");
+
+ buffer = GOKZ_BONUS_START_BUTTON_NAME_REGEX;
+ ReplaceStringEx(buffer, sizeof(buffer), "$", "");
+ StrCat(buffer, sizeof(buffer), press);
+ RE_BonusStartButton = CompileRegex(buffer);
+
+ buffer = GOKZ_BONUS_END_BUTTON_NAME_REGEX;
+ ReplaceStringEx(buffer, sizeof(buffer), "$", "");
+ StrCat(buffer, sizeof(buffer), press);
+ RE_BonusEndButton = CompileRegex(buffer);
+}
+
+
+// =====[ PRIVATE ]=====
+
+static void EntlumpSkipAllWhiteSpace(char[] entityLump, int &index)
+{
+ while (IsCharSpace(entityLump[index]) && entityLump[index] != '\0')
+ {
+ index++;
+ }
+}
+
+static int EntlumpGetString(char[] result, int maxLength, int copyCount, char[] entityLump, int entlumpIndex)
+{
+ int finalLength;
+ for (int i = 0; i < maxLength - 1 && i < copyCount; i++)
+ {
+ if (entityLump[entlumpIndex + i] == '\0')
+ {
+ break;
+ }
+ result[i] = entityLump[entlumpIndex + i];
+ finalLength++;
+ }
+
+ result[finalLength] = '\0';
+ return finalLength;
+}
+
+static EntlumpToken EntlumpGetToken(char[] entityLump, int &entlumpIndex)
+{
+ EntlumpToken result;
+
+ EntlumpSkipAllWhiteSpace(entityLump, entlumpIndex);
+
+ switch (entityLump[entlumpIndex])
+ {
+ case '{':
+ {
+ result.type = EntlumpTokenType_OpenBrace;
+ EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex);
+ entlumpIndex++;
+ }
+ case '}':
+ {
+ result.type = EntlumpTokenType_CloseBrace;
+ EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex);
+ entlumpIndex++;
+ }
+ case '\0':
+ {
+ result.type = EntlumpTokenType_EndOfStream;
+ EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex);
+ entlumpIndex++;
+ }
+ case '\"':
+ {
+ result.type = EntlumpTokenType_Identifier;
+ int identifierLen;
+ entlumpIndex++;
+ for (int i = 0; i < sizeof(result.string) - 1; i++)
+ {
+ // NOTE: Unterminated strings can probably never happen, since the map has to be
+ // loaded by the game first and the engine will fail the load before we get to it.
+ if (entityLump[entlumpIndex + i] == '\0')
+ {
+ result.type = EntlumpTokenType_Unknown;
+ break;
+ }
+ if (entityLump[entlumpIndex + i] == '\"')
+ {
+ break;
+ }
+ result.string[i] = entityLump[entlumpIndex + i];
+ identifierLen++;
+ }
+
+ entlumpIndex += identifierLen + 1; // +1 to skip over last quotation mark
+ result.string[identifierLen] = '\0';
+ }
+ default:
+ {
+ result.type = EntlumpTokenType_Unknown;
+ result.string[0] = entityLump[entlumpIndex];
+ result.string[1] = '\0';
+ }
+ }
+
+ return result;
+}
+
+static bool EntlumpParseEntity(StringMap result, char[] entityLump, int &entlumpIndex)
+{
+ EntlumpToken token;
+ token = EntlumpGetToken(entityLump, entlumpIndex);
+ if (token.type == EntlumpTokenType_EndOfStream)
+ {
+ return false;
+ }
+
+ // NOTE: The following errors will very very likely never happen, since the entity lump has to be
+ // loaded by the game first and the engine will fail the load before we get to it.
+ // But if there's an obscure bug in this code, then we'll know!!!
+ for (;;)
+ {
+ token = EntlumpGetToken(entityLump, entlumpIndex);
+ switch (token.type)
+ {
+ case EntlumpTokenType_OpenBrace:
+ {
+ continue;
+ }
+ case EntlumpTokenType_Identifier:
+ {
+ EntlumpToken valueToken;
+ valueToken = EntlumpGetToken(entityLump, entlumpIndex);
+ if (valueToken.type == EntlumpTokenType_Identifier)
+ {
+ char tempString[GOKZ_ENTLUMP_MAX_VALUE];
+ ArrayList values;
+ if (result.GetString(token.string, tempString, sizeof(tempString)))
+ {
+ result.Remove(token.string);
+ values = new ArrayList(ByteCountToCells(GOKZ_ENTLUMP_MAX_VALUE));
+ values.PushString(tempString);
+ values.PushString(valueToken.string);
+ result.SetValue(token.string, values);
+ }
+ else if (result.GetValue(token.string, values))
+ {
+ values.PushString(valueToken.string);
+ }
+ else
+ {
+ result.SetString(token.string, valueToken.string);
+ }
+ }
+ else
+ {
+ PushMappingApiError("ERROR: Entity lump: Unexpected token \"%s\".", valueToken.string);
+ return false;
+ }
+ }
+ case EntlumpTokenType_CloseBrace:
+ {
+ break;
+ }
+ case EntlumpTokenType_EndOfStream:
+ {
+ PushMappingApiError("ERROR: Entity lump: Unexpected end of entity lump! Entity lump parsing failed.");
+ return false;
+ }
+ default:
+ {
+ PushMappingApiError("ERROR: Entity lump: Invalid token \"%s\". Entity lump parsing failed.", token.string);
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+static bool GetHammerIDFromEntityStringMap(int &result, StringMap entity)
+{
+ char hammerID[32];
+ if (!entity.GetString("hammerid", hammerID, sizeof(hammerID))
+ || StringToIntEx(hammerID, result) == 0)
+ {
+ // if we don't have the hammer id, then we can't match the entity to an existing one!
+ char origin[64];
+ entity.GetString("origin", origin, sizeof(origin));
+ PushMappingApiError("ERROR: Failed to parse \"hammerid\" keyvalue on trigger! \"%i\" origin: %s.", result, origin);
+ return false;
+ }
+ return true;
+}
+
+static bool GetAntiBhopTriggerEntityProperties(AntiBhopTrigger result, StringMap entity)
+{
+ if (!GetHammerIDFromEntityStringMap(result.hammerID, entity))
+ {
+ return false;
+ }
+
+ char time[32];
+ if (!entity.GetString("climb_anti_bhop_time", time, sizeof(time))
+ || StringToFloatEx(time, result.time) == 0)
+ {
+ result.time = GOKZ_ANTI_BHOP_TRIGGER_DEFAULT_DELAY;
+ }
+
+ return true;
+}
+
+static bool GetTeleportTriggerEntityProperties(TeleportTrigger result, StringMap entity)
+{
+ if (!GetHammerIDFromEntityStringMap(result.hammerID, entity))
+ {
+ return false;
+ }
+
+ char buffer[64];
+ if (!entity.GetString("climb_teleport_type", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, view_as<int>(result.type)) == 0)
+ {
+ result.type = GOKZ_TELEPORT_TRIGGER_DEFAULT_TYPE;
+ }
+
+ if (!entity.GetString("climb_teleport_destination", result.tpDestination, sizeof(result.tpDestination)))
+ {
+ // We don't want triggers without destinations dangling about, so we need to tell everyone about it!!!
+ PushMappingApiError("ERROR: Could not find \"climb_teleport_destination\" keyvalue on a climb_teleport trigger! hammer id \"%i\".",
+ result.hammerID);
+ return false;
+ }
+
+ if (!entity.GetString("climb_teleport_delay", buffer, sizeof(buffer))
+ || StringToFloatEx(buffer, result.delay) == 0)
+ {
+ result.delay = GOKZ_TELEPORT_TRIGGER_DEFAULT_DELAY;
+ }
+
+ if (!entity.GetString("climb_teleport_use_dest_angles", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.useDestAngles) == 0)
+ {
+ result.useDestAngles = GOKZ_TELEPORT_TRIGGER_DEFAULT_USE_DEST_ANGLES;
+ }
+
+ if (!entity.GetString("climb_teleport_reset_speed", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.resetSpeed) == 0)
+ {
+ result.resetSpeed = GOKZ_TELEPORT_TRIGGER_DEFAULT_RESET_SPEED;
+ }
+
+ if (!entity.GetString("climb_teleport_reorient_player", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.reorientPlayer) == 0)
+ {
+ result.reorientPlayer = GOKZ_TELEPORT_TRIGGER_DEFAULT_REORIENT_PLAYER;
+ }
+
+ if (!entity.GetString("climb_teleport_relative", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.relativeDestination) == 0)
+ {
+ result.relativeDestination = GOKZ_TELEPORT_TRIGGER_DEFAULT_RELATIVE_DESTINATION;
+ }
+
+ // NOTE: Clamping
+ if (IsBhopTrigger(result.type))
+ {
+ result.delay = FloatMax(result.delay, GOKZ_TELEPORT_TRIGGER_BHOP_MIN_DELAY);
+ }
+ else
+ {
+ result.delay = FloatMax(result.delay, 0.0);
+ }
+
+ return true;
+}
+
+static void TimerButtonTriggerCheck(char[] touchOutput, int size, StringMap entity, StringMap timerButtonTriggers)
+{
+ int course = 0;
+ char startOutput[128];
+ char endOutput[128];
+ FormatEx(startOutput, sizeof(startOutput), "%s%s%s", GOKZ_START_BUTTON_NAME, CHAR_ESCAPE, "Press");
+ FormatEx(endOutput, sizeof(endOutput), "%s%s%s", GOKZ_END_BUTTON_NAME, CHAR_ESCAPE, "Press");
+ if (StrContains(touchOutput, startOutput, false) != -1)
+ {
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = 0;
+ trigger.isStartTimer = true;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ else if (StrContains(touchOutput, endOutput, false) != -1)
+ {
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = 0;
+ trigger.isStartTimer = false;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ else if (RE_BonusStartButton.Match(touchOutput) > 0)
+ {
+ RE_BonusStartButton.GetSubString(1, touchOutput, sizeof(size));
+ course = StringToInt(touchOutput);
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = course;
+ trigger.isStartTimer = true;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ else if (RE_BonusEndButton.Match(touchOutput) > 0)
+ {
+ RE_BonusEndButton.GetSubString(1, touchOutput, sizeof(size));
+ course = StringToInt(touchOutput);
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = course;
+ trigger.isStartTimer = false;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/prefix.sp b/sourcemod/scripting/gokz-core/map/prefix.sp
new file mode 100644
index 0000000..3ecbf89
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/prefix.sp
@@ -0,0 +1,48 @@
+/*
+ Mapping API - Prefix
+
+ Detects the map's prefix.
+*/
+
+
+
+static int currentMapPrefix;
+
+
+
+// =====[ PUBLIC ]=====
+
+int GetCurrentMapPrefix()
+{
+ return currentMapPrefix;
+}
+
+
+
+// =====[ LISTENERS ]=====
+
+void OnMapStart_Prefix()
+{
+ char map[PLATFORM_MAX_PATH], mapPrefix[PLATFORM_MAX_PATH];
+ GetCurrentMapDisplayName(map, sizeof(map));
+
+ // Get all characters before the first '_' character
+ for (int i = 0; i < sizeof(mapPrefix); i++)
+ {
+ if (map[i] == '\0' || map[i] == '_')
+ {
+ break;
+ }
+
+ mapPrefix[i] = map[i];
+ }
+
+ if (StrEqual(mapPrefix[0], "kzpro", false))
+ {
+ currentMapPrefix = MapPrefix_KZPro;
+ }
+ else
+ {
+ currentMapPrefix = MapPrefix_Other;
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/starts.sp b/sourcemod/scripting/gokz-core/map/starts.sp
new file mode 100644
index 0000000..94d5b33
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/starts.sp
@@ -0,0 +1,219 @@
+/*
+ Hooks between start destinations and GOKZ.
+*/
+
+
+
+static Regex RE_BonusStart;
+static bool startExists[GOKZ_MAX_COURSES];
+static float startOrigin[GOKZ_MAX_COURSES][3];
+static float startAngles[GOKZ_MAX_COURSES][3];
+
+// Used for SearchStart
+static Regex RE_BonusStartButton;
+static Regex RE_BonusStartZone;
+static CourseTimerType startType[GOKZ_MAX_COURSES];
+static float searchStartOrigin[GOKZ_MAX_COURSES][3];
+static float searchStartAngles[GOKZ_MAX_COURSES][3];
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapStarts()
+{
+ RE_BonusStart = CompileRegex(GOKZ_BONUS_START_NAME_REGEX);
+ RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX);
+ RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX);
+}
+
+void OnEntitySpawned_MapStarts(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+ if (!StrEqual("info_teleport_destination", buffer, false))
+ {
+ return;
+ }
+
+ if (GetEntityName(entity, buffer, sizeof(buffer)) == 0)
+ {
+ return;
+ }
+
+ if (StrEqual(GOKZ_START_NAME, buffer, false))
+ {
+ StoreStart(0, entity);
+ }
+ else
+ {
+ int course = GetStartBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ StoreStart(course, entity);
+ }
+ }
+}
+
+void OnEntitySpawnedPost_MapStarts(int entity)
+{
+ char buffer[32];
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+
+ if (StrEqual("trigger_multiple", buffer, false))
+ {
+ bool isStartZone;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false))
+ {
+ isStartZone = true;
+ StoreSearchStart(0, entity, CourseTimerType_ZoneNew);
+ }
+ else if (GetStartZoneBonusNumber(entity) != -1)
+ {
+ int course = GetStartZoneBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isStartZone = true;
+ StoreSearchStart(course, entity, CourseTimerType_ZoneNew);
+ }
+ }
+ }
+ if (!isStartZone)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer)
+ {
+ StoreSearchStart(trigger.course, entity, CourseTimerType_ZoneLegacy);
+ }
+ }
+
+ }
+ else if (StrEqual("func_button", buffer, false))
+ {
+ bool isStartButton;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false))
+ {
+ isStartButton = true;
+ StoreSearchStart(0, entity, CourseTimerType_Button);
+ }
+ else
+ {
+ int course = GetStartButtonBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isStartButton = true;
+ StoreSearchStart(course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+ if (!isStartButton)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer)
+ {
+ StoreSearchStart(trigger.course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+}
+
+void OnMapStart_MapStarts()
+{
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ startExists[course] = false;
+ startType[course] = CourseTimerType_None;
+ }
+}
+
+bool GetMapStartPosition(int course, float origin[3], float angles[3])
+{
+ if (!startExists[course])
+ {
+ return false;
+ }
+
+ origin = startOrigin[course];
+ angles = startAngles[course];
+
+ return true;
+}
+
+bool GetSearchStartPosition(int course, float origin[3], float angles[3])
+{
+ if (startType[course] == CourseTimerType_None)
+ {
+ return false;
+ }
+
+ origin = searchStartOrigin[course];
+ angles = searchStartAngles[course];
+
+ return true;
+}
+
+// =====[ PRIVATE ]=====
+
+static void StoreStart(int course, int entity)
+{
+ float origin[3], angles[3];
+ GetEntPropVector(entity, Prop_Send, "m_vecOrigin", origin);
+ GetEntPropVector(entity, Prop_Data, "m_angRotation", angles);
+ angles[2] = 0.0; // Roll should always be 0.0
+
+ startExists[course] = true;
+ startOrigin[course] = origin;
+ startAngles[course] = angles;
+}
+
+static void StoreSearchStart(int course, int entity, CourseTimerType type)
+{
+ // If StoreSearchStart is called, then there is at least an end position (even though it might not be a valid one)
+ if (startType[course] < CourseTimerType_Default)
+ {
+ startType[course] = CourseTimerType_Default;
+ }
+
+ // Real zone is always better than "fake" zones which are better than buttons
+ // as the buttons found in a map with fake zones aren't meant to be visible.
+ if (startType[course] >= type)
+ {
+ return;
+ }
+
+ float origin[3], distFromCenter[3];
+ GetEntityPositions(entity, origin, searchStartOrigin[course], searchStartAngles[course], distFromCenter);
+
+ // If it is a button or the center of the center of the zone is invalid
+ if (type == CourseTimerType_Button || !IsSpawnValid(searchStartOrigin[course]))
+ {
+ // Attempt with various positions around the entity, pick the first valid one.
+ if (!FindValidPositionAroundTimerEntity(entity, searchStartOrigin[course], searchStartAngles[course], type == CourseTimerType_Button))
+ {
+ searchStartOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone.
+ return;
+ }
+ }
+
+ // Only update the CourseTimerType if a valid position is found.
+ startType[course] = type;
+}
+
+
+static int GetStartBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStart, 1);
+}
+
+static int GetStartButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1);
+}
+
+static int GetStartZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/triggers.sp b/sourcemod/scripting/gokz-core/map/triggers.sp
new file mode 100644
index 0000000..2444493
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/triggers.sp
@@ -0,0 +1,855 @@
+/*
+ Mapping API - Triggers
+
+ Implements trigger related features.
+*/
+
+
+
+static float lastTrigMultiTouchTime[MAXPLAYERS + 1];
+static float lastTrigTeleTouchTime[MAXPLAYERS + 1];
+static float lastTouchGroundOrLadderTime[MAXPLAYERS + 1];
+static int lastTouchSingleBhopEntRef[MAXPLAYERS + 1];
+static ArrayList lastTouchSequentialBhopEntRefs[MAXPLAYERS + 1];
+static int triggerTouchCount[MAXPLAYERS + 1];
+static int antiCpTriggerTouchCount[MAXPLAYERS + 1];
+static int antiPauseTriggerTouchCount[MAXPLAYERS + 1];
+static int antiJumpstatTriggerTouchCount[MAXPLAYERS + 1];
+static int mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+static int bhopTouchCount[MAXPLAYERS + 1];
+static bool jumpedThisTick[MAXPLAYERS + 1];
+static float jumpOrigin[MAXPLAYERS + 1][3];
+static float jumpVelocity[MAXPLAYERS + 1][3];
+static ArrayList triggerTouchList[MAXPLAYERS + 1]; // arraylist of TouchedTrigger that the player is currently touching. this array won't ever get long (unless the mapper does something weird).
+static StringMap triggerTouchCounts[MAXPLAYERS + 1]; // stringmap of int touch counts with key being a string of the entity reference.
+static StringMap antiBhopTriggers; // stringmap of AntiBhopTrigger with key being a string of the m_iHammerID entprop.
+static StringMap teleportTriggers; // stringmap of TeleportTrigger with key being a string of the m_iHammerID entprop.
+static StringMap timerButtonTriggers; // stringmap of legacy timer zone triggers with key being a string of the m_iHammerID entprop.
+static ArrayList parseErrorStrings;
+
+
+
+// =====[ PUBLIC ]=====
+
+bool BhopTriggersJustTouched(int client)
+{
+ // NOTE: This is slightly incorrect since we touch triggers in the air, but
+ // it doesn't matter since we can't checkpoint in the air.
+ if (bhopTouchCount[client] > 0)
+ {
+ return true;
+ }
+ // GetEngineTime return changes between calls. We only call it once at the beginning.
+ float engineTime = GetEngineTime();
+ // If the player touches a teleport trigger, increase the delay required
+ if (engineTime - lastTouchGroundOrLadderTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched ground or ladder
+ && engineTime - lastTrigMultiTouchTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched trigger_multiple
+ || engineTime - lastTrigTeleTouchTime[client] < GOKZ_BHOP_NO_CHECKPOINT_TIME) // Just touched trigger_teleport
+ {
+ return true;
+ }
+
+ return Movement_GetMovetype(client) == MOVETYPE_LADDER
+ && triggerTouchCount[client] > 0
+ && engineTime - lastTrigTeleTouchTime[client] < GOKZ_LADDER_NO_CHECKPOINT_TIME;
+}
+
+bool AntiCpTriggerIsTouched(int client)
+{
+ return antiCpTriggerTouchCount[client] > 0;
+}
+
+bool AntiPauseTriggerIsTouched(int client)
+{
+ return antiPauseTriggerTouchCount[client] > 0;
+}
+
+void PushMappingApiError(char[] format, any ...)
+{
+ char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH];
+ VFormat(error, sizeof(error), format, 2);
+ parseErrorStrings.PushString(error);
+}
+
+TriggerType GetTriggerType(char[] targetName)
+{
+ TriggerType result = TriggerType_Invalid;
+
+ if (StrEqual(targetName, GOKZ_ANTI_BHOP_TRIGGER_NAME))
+ {
+ result = TriggerType_Antibhop;
+ }
+ else if (StrEqual(targetName, GOKZ_TELEPORT_TRIGGER_NAME))
+ {
+ result = TriggerType_Teleport;
+ }
+
+ return result;
+}
+
+bool IsBhopTrigger(TeleportType type)
+{
+ return type == TeleportType_MultiBhop
+ || type == TeleportType_SingleBhop
+ || type == TeleportType_SequentialBhop;
+}
+
+bool IsTimerButtonTrigger(int entity, TimerButtonTrigger trigger)
+{
+ char hammerID[32];
+ bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID));
+ if (gotHammerID && timerButtonTriggers.GetArray(hammerID, trigger, sizeof(trigger)))
+ {
+ return true;
+ }
+ return false;
+}
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapTriggers()
+{
+ parseErrorStrings = new ArrayList(ByteCountToCells(GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH));
+ antiBhopTriggers = new StringMap();
+ teleportTriggers = new StringMap();
+ timerButtonTriggers = new StringMap();
+}
+
+void OnMapStart_MapTriggers()
+{
+ parseErrorStrings.Clear();
+ antiBhopTriggers.Clear();
+ teleportTriggers.Clear();
+ timerButtonTriggers.Clear();
+ mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+ EntlumpParse(antiBhopTriggers, teleportTriggers, timerButtonTriggers, mapMappingApiVersion);
+
+ if (mapMappingApiVersion > GOKZ_MAPPING_API_VERSION)
+ {
+ SetFailState("Map's mapping api version is too big! Maximum supported version is %i, but map has %i. If you're not on the latest GOKZ version, then update!",
+ GOKZ_MAPPING_API_VERSION, mapMappingApiVersion);
+ }
+}
+
+void OnClientPutInServer_MapTriggers(int client)
+{
+ triggerTouchCount[client] = 0;
+ antiCpTriggerTouchCount[client] = 0;
+ antiPauseTriggerTouchCount[client] = 0;
+ antiJumpstatTriggerTouchCount[client] = 0;
+
+ if (triggerTouchList[client] == null)
+ {
+ triggerTouchList[client] = new ArrayList(sizeof(TouchedTrigger));
+ }
+ else
+ {
+ triggerTouchList[client].Clear();
+ }
+
+ if (triggerTouchCounts[client] == null)
+ {
+ triggerTouchCounts[client] = new StringMap();
+ }
+ else
+ {
+ triggerTouchCounts[client].Clear();
+ }
+
+ bhopTouchCount[client] = 0;
+
+ if (lastTouchSequentialBhopEntRefs[client] == null)
+ {
+ lastTouchSequentialBhopEntRefs[client] = new ArrayList();
+ }
+ else
+ {
+ lastTouchSequentialBhopEntRefs[client].Clear();
+ }
+}
+
+void OnPlayerRunCmd_MapTriggers(int client, int &buttons)
+{
+ int flags = GetEntityFlags(client);
+ MoveType moveType = GetEntityMoveType(client);
+
+ // if the player isn't touching any bhop triggers on ground/a ladder, then
+ // reset the singlebhop and sequential bhop state.
+ if ((flags & FL_ONGROUND || moveType == MOVETYPE_LADDER)
+ && bhopTouchCount[client] == 0)
+ {
+ ResetBhopState(client);
+ }
+
+ if (antiJumpstatTriggerTouchCount[client] > 0)
+ {
+ if (GetFeatureStatus(FeatureType_Native, "GOKZ_JS_InvalidateJump") == FeatureStatus_Available)
+ {
+ GOKZ_JS_InvalidateJump(client);
+ }
+ }
+
+ // Check if we're touching any triggers and act accordingly.
+ // NOTE: Read through the touch list in reverse order, so some
+ // trigger behaviours will be better. Trust me!
+ int triggerTouchListLength = triggerTouchList[client].Length;
+ for (int i = triggerTouchListLength - 1; i >= 0; i--)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+
+ if (touched.triggerType == TriggerType_Antibhop)
+ {
+ TouchAntibhopTrigger(client, touched, buttons, flags);
+ }
+ else if (touched.triggerType == TriggerType_Teleport)
+ {
+ // Sometimes due to lag or whatever, the player can be
+ // teleported twice by the same trigger. This fixes that.
+ if (TouchTeleportTrigger(client, touched, flags))
+ {
+ RemoveTriggerFromTouchList(client, EntRefToEntIndex(touched.entRef));
+ i--;
+ triggerTouchListLength--;
+ }
+ }
+ }
+ jumpedThisTick[client] = false;
+}
+
+void OnPlayerSpawn_MapTriggers(int client)
+{
+ // Print trigger errors every time a player spawns so that
+ // mappers and testers can very easily spot mistakes in names
+ // and get them fixed asap.
+ if (parseErrorStrings.Length > 0)
+ {
+ char errStart[] = "ERROR: Errors detected when trying to load triggers!";
+ CPrintToChat(client, "{red}%s", errStart);
+ PrintToConsole(client, "\n%s", errStart);
+
+ int length = parseErrorStrings.Length;
+ for (int err = 0; err < length; err++)
+ {
+ char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH];
+ parseErrorStrings.GetString(err, error, sizeof(error));
+ CPrintToChat(client, "{red}%s", error);
+ PrintToConsole(client, error);
+ }
+ CPrintToChat(client, "{red}If the errors get clipped off in the chat, then look in your developer console!\n");
+ }
+}
+
+public void OnPlayerJump_Triggers(int client)
+{
+ jumpedThisTick[client] = true;
+ GetClientAbsOrigin(client, jumpOrigin[client]);
+ Movement_GetVelocity(client, jumpVelocity[client]);
+}
+
+void OnEntitySpawned_MapTriggers(int entity)
+{
+ char classname[32];
+ GetEntityClassname(entity, classname, sizeof(classname));
+ char name[64];
+ GetEntityName(entity, name, sizeof(name));
+
+ bool triggerMultiple = StrEqual("trigger_multiple", classname);
+ if (triggerMultiple)
+ {
+ char hammerID[32];
+ bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID));
+
+ if (StrEqual(GOKZ_TELEPORT_TRIGGER_NAME, name))
+ {
+ TeleportTrigger teleportTrigger;
+ if (gotHammerID && teleportTriggers.GetArray(hammerID, teleportTrigger, sizeof(teleportTrigger)))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnTeleportTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnTeleportTrigTouchEnd_MapTriggers);
+ }
+ else
+ {
+ PushMappingApiError("ERROR: Couldn't match teleport trigger's Hammer ID %s with any Hammer ID from the map.", hammerID);
+ }
+ }
+ else if (StrEqual(GOKZ_ANTI_BHOP_TRIGGER_NAME, name))
+ {
+ AntiBhopTrigger antiBhopTrigger;
+ if (gotHammerID && antiBhopTriggers.GetArray(hammerID, antiBhopTrigger, sizeof(antiBhopTrigger)))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiBhopTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiBhopTrigTouchEnd_MapTriggers);
+ }
+ else
+ {
+ PushMappingApiError("ERROR: Couldn't match antibhop trigger's Hammer ID %s with any Hammer ID from the map.", hammerID);
+ }
+ }
+ else if (StrEqual(GOKZ_BHOP_RESET_TRIGGER_NAME, name))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnBhopResetTouchStart_MapTriggers);
+ }
+ else if (StrEqual(GOKZ_ANTI_CP_TRIGGER_NAME, name, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiCpTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiCpTrigTouchEnd_MapTriggers);
+ }
+ else if (StrEqual(GOKZ_ANTI_PAUSE_TRIGGER_NAME, name, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiPauseTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiPauseTrigTouchEnd_MapTriggers);
+ }
+ else if (StrEqual(GOKZ_ANTI_JUMPSTAT_TRIGGER_NAME, name, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiJumpstatTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiJumpstatTrigTouchEnd_MapTriggers);
+ }
+ else
+ {
+ // NOTE: SDKHook touch hooks bypass trigger filters. We want that only with
+ // non mapping api triggers because it prevents checkpointing on bhop blocks.
+ SDKHook(entity, SDKHook_StartTouchPost, OnTrigMultTouchStart_MapTriggers);
+ SDKHook(entity, SDKHook_EndTouchPost, OnTrigMultTouchEnd_MapTriggers);
+ }
+ }
+ else if (StrEqual("trigger_teleport", classname))
+ {
+ SDKHook(entity, SDKHook_StartTouchPost, OnTrigTeleTouchStart_MapTriggers);
+ SDKHook(entity, SDKHook_EndTouchPost, OnTrigTeleTouchEnd_MapTriggers);
+ }
+}
+
+public void OnAntiBhopTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ int touchCount = IncrementTriggerTouchCount(other, entity);
+ if (touchCount <= 0)
+ {
+ // The trigger has fired a matching endtouch output before
+ // the starttouch output, so ignore it.
+ return;
+ }
+
+ if (jumpedThisTick[other])
+ {
+ TeleportEntity(other, jumpOrigin[other], NULL_VECTOR, jumpVelocity[other]);
+ }
+
+ AddTriggerToTouchList(other, entity, TriggerType_Antibhop);
+}
+
+public void OnAntiBhopTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ DecrementTriggerTouchCount(other, entity);
+ RemoveTriggerFromTouchList(other, entity);
+}
+
+public void OnTeleportTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ int touchCount = IncrementTriggerTouchCount(other, entity);
+ if (touchCount <= 0)
+ {
+ // The trigger has fired a matching endtouch output before
+ // the starttouch output, so ignore it.
+ return;
+ }
+
+ char key[32];
+ GetEntityHammerIDString(entity, key, sizeof(key));
+ TeleportTrigger trigger;
+ if (teleportTriggers.GetArray(key, trigger, sizeof(trigger))
+ && IsBhopTrigger(trigger.type))
+ {
+ bhopTouchCount[other]++;
+ }
+
+ AddTriggerToTouchList(other, entity, TriggerType_Teleport);
+}
+
+public void OnTeleportTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ DecrementTriggerTouchCount(other, entity);
+
+ char key[32];
+ GetEntityHammerIDString(entity, key, sizeof(key));
+ TeleportTrigger trigger;
+ if (teleportTriggers.GetArray(key, trigger, sizeof(trigger))
+ && IsBhopTrigger(trigger.type))
+ {
+ bhopTouchCount[other]--;
+ }
+
+ RemoveTriggerFromTouchList(other, entity);
+}
+
+public void OnBhopResetTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ ResetBhopState(other);
+}
+
+public void OnAntiCpTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiCpTriggerTouchCount[other]++;
+}
+
+public void OnAntiCpTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiCpTriggerTouchCount[other]--;
+}
+
+public void OnAntiPauseTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiPauseTriggerTouchCount[other]++;
+}
+
+public void OnAntiPauseTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiPauseTriggerTouchCount[other]--;
+}
+
+public void OnAntiJumpstatTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiJumpstatTriggerTouchCount[other]++;
+}
+
+public void OnAntiJumpstatTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiJumpstatTriggerTouchCount[other]--;
+}
+
+public void OnTrigMultTouchStart_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ lastTrigMultiTouchTime[other] = GetEngineTime();
+ triggerTouchCount[other]++;
+}
+
+public void OnTrigMultTouchEnd_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ triggerTouchCount[other]--;
+}
+
+public void OnTrigTeleTouchStart_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ lastTrigTeleTouchTime[other] = GetEngineTime();
+ triggerTouchCount[other]++;
+}
+
+public void OnTrigTeleTouchEnd_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ triggerTouchCount[other]--;
+}
+
+void OnStartTouchGround_MapTriggers(int client)
+{
+ lastTouchGroundOrLadderTime[client] = GetEngineTime();
+
+ for (int i = 0; i < triggerTouchList[client].Length; i++)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+ // set the touched tick to the tick that the player touches the ground.
+ touched.groundTouchTick = gI_TickCount[client];
+ triggerTouchList[client].SetArray(i, touched);
+ }
+}
+
+void OnStopTouchGround_MapTriggers(int client)
+{
+ for (int i = 0; i < triggerTouchList[client].Length; i++)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+
+ if (touched.triggerType == TriggerType_Teleport)
+ {
+ char key[32];
+ GetEntityHammerIDString(touched.entRef, key, sizeof(key));
+ TeleportTrigger trigger;
+ // set last touched triggers for single and sequential bhop.
+ if (teleportTriggers.GetArray(key, trigger, sizeof(trigger))
+ && IsBhopTrigger(trigger.type))
+ {
+ if (trigger.type == TeleportType_SequentialBhop)
+ {
+ lastTouchSequentialBhopEntRefs[client].Push(touched.entRef);
+ }
+ // NOTE: For singlebhops, we don't care which type of bhop we last touched, because
+ // otherwise jumping back and forth between a multibhop and a singlebhop wouldn't work.
+ if (i == 0 && IsBhopTrigger(trigger.type))
+ {
+ // We only want to set this once in this loop.
+ lastTouchSingleBhopEntRef[client] = touched.entRef;
+ }
+ }
+ }
+ }
+}
+
+void OnChangeMovetype_MapTriggers(int client, MoveType newMovetype)
+{
+ if (newMovetype == MOVETYPE_LADDER)
+ {
+ lastTouchGroundOrLadderTime[client] = GetEngineTime();
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void AddTriggerToTouchList(int client, int trigger, TriggerType triggerType)
+{
+ int triggerEntRef = EntIndexToEntRef(trigger);
+
+ TouchedTrigger touched;
+ touched.triggerType = triggerType;
+ touched.entRef = triggerEntRef;
+ touched.startTouchTick = gI_TickCount[client];
+ touched.groundTouchTick = -1;
+ if (GetEntityFlags(client) & FL_ONGROUND)
+ {
+ touched.groundTouchTick = gI_TickCount[client];
+ }
+
+ triggerTouchList[client].PushArray(touched);
+}
+
+static void RemoveTriggerFromTouchList(int client, int trigger)
+{
+ int triggerEntRef = EntIndexToEntRef(trigger);
+ for (int i = 0; i < triggerTouchList[client].Length; i++)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+ if (touched.entRef == triggerEntRef)
+ {
+ triggerTouchList[client].Erase(i);
+ break;
+ }
+ }
+}
+
+static int IncrementTriggerTouchCount(int client, int trigger)
+{
+ int entref = EntIndexToEntRef(trigger);
+ char szEntref[64];
+ FormatEx(szEntref, sizeof(szEntref), "%i", entref);
+
+ int value = 0;
+ triggerTouchCounts[client].GetValue(szEntref, value);
+
+ value += 1;
+ triggerTouchCounts[client].SetValue(szEntref, value);
+
+ return value;
+}
+
+static void DecrementTriggerTouchCount(int client, int trigger)
+{
+ int entref = EntIndexToEntRef(trigger);
+ char szEntref[64];
+ FormatEx(szEntref, sizeof(szEntref), "%i", entref);
+
+ int value = 0;
+ triggerTouchCounts[client].GetValue(szEntref, value);
+
+ value -= 1;
+ triggerTouchCounts[client].SetValue(szEntref, value);
+}
+
+static void TouchAntibhopTrigger(int client, TouchedTrigger touched, int &newButtons, int flags)
+{
+ if (!(flags & FL_ONGROUND))
+ {
+ // Disable jump when the player is in the air.
+ // This is a very simple way to fix jumpbugging antibhop triggers.
+ newButtons &= ~IN_JUMP;
+ return;
+ }
+
+ if (touched.groundTouchTick == -1)
+ {
+ // The player hasn't touched the ground inside this trigger yet.
+ return;
+ }
+
+ char key[32];
+ GetEntityHammerIDString(touched.entRef, key, sizeof(key));
+ AntiBhopTrigger trigger;
+ if (antiBhopTriggers.GetArray(key, trigger, sizeof(trigger)))
+ {
+ float touchTime = CalculateGroundTouchTime(client, touched);
+ if (trigger.time == 0.0 || touchTime <= trigger.time)
+ {
+ // disable jump
+ newButtons &= ~IN_JUMP;
+ }
+ }
+}
+
+static bool TouchTeleportTrigger(int client, TouchedTrigger touched, int flags)
+{
+ bool shouldTeleport = false;
+
+ char key[32];
+ GetEntityHammerIDString(touched.entRef, key, sizeof(key));
+ TeleportTrigger trigger;
+ if (!teleportTriggers.GetArray(key, trigger, sizeof(trigger)))
+ {
+ // Couldn't get the teleport trigger from the trigger array for some reason.
+ return shouldTeleport;
+ }
+
+ bool isBhopTrigger = IsBhopTrigger(trigger.type);
+ // NOTE: Player hasn't touched the ground inside this trigger yet.
+ if (touched.groundTouchTick == -1 && isBhopTrigger)
+ {
+ return shouldTeleport;
+ }
+
+ float destOrigin[3];
+ float destAngles[3];
+ bool gotDestOrigin;
+ bool gotDestAngles;
+ int destinationEnt = GetTeleportDestinationAndOrientation(trigger.tpDestination, destOrigin, destAngles, gotDestOrigin, gotDestAngles);
+
+ float triggerOrigin[3];
+ bool gotTriggerOrigin = GetEntityAbsOrigin(touched.entRef, triggerOrigin);
+
+ // NOTE: We only use the trigger's origin if we're using a relative destination, so if
+ // we're not using a relative destination and don't have it, then it's fine.
+ if (!IsValidEntity(destinationEnt) || !gotDestOrigin
+ || (!gotTriggerOrigin && trigger.relativeDestination))
+ {
+ PrintToConsole(client, "[KZ] Invalid teleport destination \"%s\" on trigger with hammerID %i.", trigger.tpDestination, trigger.hammerID);
+ return shouldTeleport;
+ }
+
+ // NOTE: Find out if we should actually teleport.
+ if (isBhopTrigger && (flags & FL_ONGROUND))
+ {
+ float touchTime = CalculateGroundTouchTime(client, touched);
+ if (touchTime > trigger.delay)
+ {
+ shouldTeleport = true;
+ }
+ else if (trigger.type == TeleportType_SingleBhop)
+ {
+ shouldTeleport = lastTouchSingleBhopEntRef[client] == touched.entRef;
+ }
+ else if (trigger.type == TeleportType_SequentialBhop)
+ {
+ int length = lastTouchSequentialBhopEntRefs[client].Length;
+ for (int j = 0; j < length; j++)
+ {
+ int entRef = lastTouchSequentialBhopEntRefs[client].Get(j);
+ if (entRef == touched.entRef)
+ {
+ shouldTeleport = true;
+ break;
+ }
+ }
+ }
+ }
+ else if (trigger.type == TeleportType_Normal)
+ {
+ float touchTime = CalculateStartTouchTime(client, touched);
+ shouldTeleport = touchTime > trigger.delay || (trigger.delay == 0.0);
+ }
+
+ if (!shouldTeleport)
+ {
+ return shouldTeleport;
+ }
+
+ bool shouldReorientPlayer = trigger.reorientPlayer
+ && gotDestAngles && (destAngles[1] != 0.0);
+
+ float zAxis[3];
+ zAxis = view_as<float>({0.0, 0.0, 1.0});
+
+ // NOTE: Work out finalOrigin.
+ float finalOrigin[3];
+ if (trigger.relativeDestination)
+ {
+ float playerOrigin[3];
+ Movement_GetOrigin(client, playerOrigin);
+
+ float playerOffsetFromTrigger[3];
+ SubtractVectors(playerOrigin, triggerOrigin, playerOffsetFromTrigger);
+
+ if (shouldReorientPlayer)
+ {
+ // NOTE: rotate player offset by the destination trigger's yaw.
+ RotateVectorAxis(playerOffsetFromTrigger, zAxis, DegToRad(destAngles[1]), playerOffsetFromTrigger);
+ }
+
+ AddVectors(destOrigin, playerOffsetFromTrigger, finalOrigin);
+ }
+ else
+ {
+ finalOrigin = destOrigin;
+ }
+
+ // NOTE: Work out finalPlayerAngles.
+ float finalPlayerAngles[3];
+ Movement_GetEyeAngles(client, finalPlayerAngles);
+ if (shouldReorientPlayer)
+ {
+ finalPlayerAngles[1] -= destAngles[1];
+
+ float velocity[3];
+ Movement_GetVelocity(client, velocity);
+
+ // NOTE: rotate velocity by the destination trigger's yaw.
+ RotateVectorAxis(velocity, zAxis, DegToRad(destAngles[1]), velocity);
+ Movement_SetVelocity(client, velocity);
+ }
+ else if (!trigger.reorientPlayer && trigger.useDestAngles)
+ {
+ finalPlayerAngles = destAngles;
+ }
+
+ if (shouldTeleport)
+ {
+ TeleportPlayer(client, finalOrigin, finalPlayerAngles, gotDestAngles && trigger.useDestAngles, trigger.resetSpeed);
+ }
+
+ return shouldTeleport;
+}
+
+static float CalculateGroundTouchTime(int client, TouchedTrigger touched)
+{
+ float result = float(gI_TickCount[client] - touched.groundTouchTick) * GetTickInterval();
+ return result;
+}
+
+static float CalculateStartTouchTime(int client, TouchedTrigger touched)
+{
+ float result = float(gI_TickCount[client] - touched.startTouchTick) * GetTickInterval();
+ return result;
+}
+
+static void ResetBhopState(int client)
+{
+ lastTouchSingleBhopEntRef[client] = INVALID_ENT_REFERENCE;
+ lastTouchSequentialBhopEntRefs[client].Clear();
+}
+
+static bool GetEntityHammerIDString(int entity, char[] buffer, int maxLength)
+{
+ if (!IsValidEntity(entity))
+ {
+ return false;
+ }
+
+ if (!HasEntProp(entity, Prop_Data, "m_iHammerID"))
+ {
+ return false;
+ }
+
+ int hammerID = GetEntProp(entity, Prop_Data, "m_iHammerID");
+ IntToString(hammerID, buffer, maxLength);
+
+ return true;
+}
+
+// NOTE: returns an entity reference (possibly invalid).
+static int GetTeleportDestinationAndOrientation(char[] targetName, float origin[3], float angles[3] = NULL_VECTOR, bool &gotOrigin = false, bool &gotAngles = false)
+{
+ // NOTE: We're not caching the teleport destination because it could change.
+ int destination = GOKZFindEntityByName(targetName, .ignorePlayers = true);
+ if (!IsValidEntity(destination))
+ {
+ return destination;
+ }
+
+ gotOrigin = GetEntityAbsOrigin(destination, origin);
+
+ if (HasEntProp(destination, Prop_Data, "m_angAbsRotation"))
+ {
+ GetEntPropVector(destination, Prop_Data, "m_angAbsRotation", angles);
+ gotAngles = true;
+ }
+ else
+ {
+ gotAngles = false;
+ }
+
+ return destination;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/zones.sp b/sourcemod/scripting/gokz-core/map/zones.sp
new file mode 100644
index 0000000..684e42d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/zones.sp
@@ -0,0 +1,183 @@
+/*
+ Hooks between specifically named trigger_multiples and GOKZ.
+*/
+
+
+
+static Regex RE_BonusStartZone;
+static Regex RE_BonusEndZone;
+static bool touchedGroundSinceTouchingStartZone[MAXPLAYERS + 1];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapZones()
+{
+ RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX);
+ RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX);
+}
+
+void OnStartTouchGround_MapZones(int client)
+{
+ touchedGroundSinceTouchingStartZone[client] = true;
+}
+
+void OnEntitySpawned_MapZones(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+ if (!StrEqual("trigger_multiple", buffer, false))
+ {
+ return;
+ }
+
+ if (GetEntityName(entity, buffer, sizeof(buffer)) == 0)
+ {
+ return;
+ }
+
+ int course = 0;
+ if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnStartZoneStartTouch);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnStartZoneEndTouch);
+ RegisterCourseStart(course);
+ }
+ else if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnEndZoneStartTouch);
+ RegisterCourseEnd(course);
+ }
+ else if ((course = GetStartZoneBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnBonusStartZoneStartTouch);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnBonusStartZoneEndTouch);
+ RegisterCourseStart(course);
+ }
+ else if ((course = GetEndZoneBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnBonusEndZoneStartTouch);
+ RegisterCourseEnd(course);
+ }
+}
+
+public void OnStartZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessStartZoneStartTouch(activator, 0);
+}
+
+public void OnStartZoneEndTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessStartZoneEndTouch(activator, 0);
+}
+
+public void OnEndZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessEndZoneStartTouch(activator, 0);
+}
+
+public void OnBonusStartZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetStartZoneBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessStartZoneStartTouch(activator, course);
+}
+
+public void OnBonusStartZoneEndTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetStartZoneBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessStartZoneEndTouch(activator, course);
+}
+
+public void OnBonusEndZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetEndZoneBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessEndZoneStartTouch(activator, course);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void ProcessStartZoneStartTouch(int client, int course)
+{
+ touchedGroundSinceTouchingStartZone[client] = Movement_GetOnGround(client);
+
+ GOKZ_StopTimer(client, false);
+ SetCurrentCourse(client, course);
+
+ OnStartZoneStartTouch_Teleports(client, course);
+}
+
+static void ProcessStartZoneEndTouch(int client, int course)
+{
+ if (!touchedGroundSinceTouchingStartZone[client])
+ {
+ return;
+ }
+
+ GOKZ_StartTimer(client, course, true);
+ GOKZ_ResetVirtualButtonPosition(client, true);
+}
+
+static void ProcessEndZoneStartTouch(int client, int course)
+{
+ GOKZ_EndTimer(client, course);
+ GOKZ_ResetVirtualButtonPosition(client, false);
+}
+
+static int GetStartZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1);
+}
+
+static int GetEndZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/menus/mode_menu.sp b/sourcemod/scripting/gokz-core/menus/mode_menu.sp
new file mode 100644
index 0000000..934d29c
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/menus/mode_menu.sp
@@ -0,0 +1,40 @@
+/*
+ Lets players choose their mode.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_Mode);
+ menu.SetTitle("%T", "Mode Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, true);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public int MenuHandler_Mode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ GOKZ_SetCoreOption(param1, Option_Mode, param2);
+ if (GetCameFromOptionsMenu(param1))
+ {
+ DisplayOptionsMenu(param1, TopMenuPosition_LastCategory);
+ }
+ }
+ else if (action == MenuAction_Cancel && GetCameFromOptionsMenu(param1))
+ {
+ DisplayOptionsMenu(param1, TopMenuPosition_LastCategory);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/menus/options_menu.sp b/sourcemod/scripting/gokz-core/menus/options_menu.sp
new file mode 100644
index 0000000..240ee81
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/menus/options_menu.sp
@@ -0,0 +1,174 @@
+/*
+ TopMenu that allows users to browse categories of options.
+
+ Adds core options to the general category where players
+ can cycle the value of each core option.
+*/
+
+
+
+static TopMenu optionsMenu;
+static TopMenuObject catGeneral;
+static TopMenuObject itemsGeneral[OPTION_COUNT];
+static bool cameFromOptionsMenu[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayOptionsMenu(int client, TopMenuPosition position = TopMenuPosition_Start)
+{
+ optionsMenu.Display(client, position);
+ cameFromOptionsMenu[client] = false;
+}
+
+TopMenu GetOptionsTopMenu()
+{
+ return optionsMenu;
+}
+
+bool GetCameFromOptionsMenu(int client)
+{
+ return cameFromOptionsMenu[client];
+}
+
+
+
+// =====[ LISTENERS ]=====
+
+void OnAllPluginsLoaded_OptionsMenu()
+{
+ optionsMenu = new TopMenu(TopMenuHandler_Options);
+ Call_GOKZ_OnOptionsMenuCreated(optionsMenu);
+ Call_GOKZ_OnOptionsMenuReady(optionsMenu);
+}
+
+void OnConfigsExecuted_OptionsMenu()
+{
+ SortOptionsMenu();
+}
+
+void OnOptionsMenuCreated_OptionsMenu()
+{
+ catGeneral = optionsMenu.AddCategory(GENERAL_OPTION_CATEGORY, TopMenuHandler_Options);
+}
+
+void OnOptionsMenuReady_OptionsMenu()
+{
+ for (int option = 0; option < view_as<int>(OPTION_COUNT); option++)
+ {
+ if (option == view_as<int>(Option_Style))
+ {
+ continue; // TODO Currently hard-coded to skip style
+ }
+ itemsGeneral[option] = optionsMenu.AddItem(gC_CoreOptionNames[option], TopMenuHandler_General, catGeneral);
+ }
+}
+
+
+
+// =====[ HANDLER ]=====
+
+public void TopMenuHandler_Options(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == INVALID_TOPMENUOBJECT)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - Title", param);
+ }
+ else if (topobj_id == catGeneral)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - General", param);
+ }
+ }
+}
+
+public void TopMenuHandler_General(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ Option option = OPTION_INVALID;
+ for (int i = 0; i < view_as<int>(OPTION_COUNT); i++)
+ {
+ if (topobj_id == itemsGeneral[i])
+ {
+ option = view_as<Option>(i);
+ break;
+ }
+ }
+
+ if (option == OPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case Option_Mode:
+ {
+ FormatEx(buffer, maxlength, "%T - %s",
+ gC_CoreOptionPhrases[option], param,
+ gC_ModeNames[GOKZ_GetCoreOption(param, option)]);
+ }
+ case Option_TimerButtonZoneType:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], param,
+ gC_TimerButtonZoneTypePhrases[GOKZ_GetCoreOption(param, option)], param);
+ }
+ case Option_Safeguard:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], param,
+ gC_SafeGuardPhrases[GOKZ_GetCoreOption(param, option)], param);
+ }
+ default:FormatToggleableOptionDisplay(param, option, buffer, maxlength);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ switch (option)
+ {
+ case Option_Mode:
+ {
+ cameFromOptionsMenu[param] = true;
+ DisplayModeMenu(param);
+ }
+ default:
+ {
+ GOKZ_CycleCoreOption(param, option);
+ optionsMenu.Display(param, TopMenuPosition_LastCategory);
+ }
+ }
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void SortOptionsMenu()
+{
+ char error[256];
+ if (!optionsMenu.LoadConfig(GOKZ_CFG_OPTIONS_SORTING, error, sizeof(error)))
+ {
+ LogError("Failed to load file: \"%s\". Error: %s", GOKZ_CFG_OPTIONS_SORTING, error);
+ }
+}
+
+static void FormatToggleableOptionDisplay(int client, Option option, char[] buffer, int maxlength)
+{
+ if (GOKZ_GetCoreOption(client, option) == 0)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], client,
+ "Options Menu - Enabled", client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/misc.sp b/sourcemod/scripting/gokz-core/misc.sp
new file mode 100644
index 0000000..a117880
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/misc.sp
@@ -0,0 +1,803 @@
+/*
+ Small features that aren't worth splitting into their own file.
+*/
+
+
+
+// =====[ GOKZ.CFG ]=====
+
+void OnMapStart_KZConfig()
+{
+ char gokzCfgFullPath[PLATFORM_MAX_PATH];
+ FormatEx(gokzCfgFullPath, sizeof(gokzCfgFullPath), "cfg/%s", GOKZ_CFG_SERVER);
+
+ if (FileExists(gokzCfgFullPath))
+ {
+ ServerCommand("exec %s", GOKZ_CFG_SERVER);
+ }
+ else
+ {
+ SetFailState("Failed to load file: \"%s\". Check that it exists.", gokzCfgFullPath);
+ }
+}
+
+
+
+// =====[ GODMODE ]=====
+
+void OnPlayerSpawn_GodMode(int client)
+{
+ // Stop players from taking damage
+ SetEntProp(client, Prop_Data, "m_takedamage", 0);
+ SetEntityFlags(client, GetEntityFlags(client) | FL_GODMODE);
+}
+
+
+
+// =====[ NOCLIP ]=====
+
+int noclipReleaseTime[MAXPLAYERS + 1];
+
+void ToggleNoclip(int client)
+{
+ if (Movement_GetMovetype(client) != MOVETYPE_NOCLIP)
+ {
+ EnableNoclip(client);
+ }
+ else
+ {
+ DisableNoclip(client);
+ }
+}
+
+void EnableNoclip(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client))
+ {
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+ Movement_SetMovetype(client, MOVETYPE_NOCLIP);
+ GOKZ_StopTimer(client);
+ }
+}
+
+void DisableNoclip(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP)
+ {
+ noclipReleaseTime[client] = GetGameTickCount();
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Prevents an exploit that would let you noclip out of start zones
+ RemoveNoclipGroundFlag(client);
+ }
+}
+
+void ToggleNoclipNotrigger(int client)
+{
+ if (Movement_GetMovetype(client) != MOVETYPE_NOCLIP)
+ {
+ EnableNoclipNotrigger(client);
+ }
+ else
+ {
+ DisableNoclipNotrigger(client);
+ }
+}
+
+void EnableNoclipNotrigger(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client))
+ {
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+ Movement_SetMovetype(client, MOVETYPE_NOCLIP);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER);
+ GOKZ_StopTimer(client);
+ }
+}
+
+void DisableNoclipNotrigger(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP)
+ {
+ noclipReleaseTime[client] = GetGameTickCount();
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Prevents an exploit that would let you noclip out of start zones
+ RemoveNoclipGroundFlag(client);
+ }
+}
+
+void RemoveNoclipGroundFlag(int client)
+{
+ float startPosition[3], endPosition[3];
+ GetClientAbsOrigin(client, startPosition);
+ endPosition = startPosition;
+ endPosition[2] = startPosition[2] - 2.0;
+ Handle trace = TR_TraceHullFilterEx(
+ startPosition,
+ endPosition,
+ view_as<float>( { -16.0, -16.0, 0.0 } ),
+ view_as<float>( { 16.0, 16.0, 72.0 } ),
+ MASK_PLAYERSOLID,
+ TraceEntityFilterPlayers,
+ client);
+
+ if (!TR_DidHit(trace))
+ {
+ SetEntityFlags(client, GetEntityFlags(client) & ~FL_ONGROUND);
+ }
+ delete trace;
+}
+
+bool JustNoclipped(int client)
+{
+ return GetGameTickCount() - noclipReleaseTime[client] <= GOKZ_TIMER_START_NOCLIP_TICKS;
+}
+
+void OnClientPutInServer_Noclip(int client)
+{
+ noclipReleaseTime[client] = 0;
+}
+
+// =====[ TURNBINDS ]=====
+
+static int turnbindsLastLeftStart[MAXPLAYERS + 1];
+static int turnbindsLastRightStart[MAXPLAYERS + 1];
+static float turnbindsLastValidYaw[MAXPLAYERS + 1];
+static int turnbindsOldButtons[MAXPLAYERS + 1];
+
+void OnClientPutInServer_Turnbinds(int client)
+{
+ turnbindsLastLeftStart[client] = 0;
+ turnbindsLastRightStart[client] = 0;
+}
+// Ensures that there is a minimum time between starting to turnbind in one direction
+// and then starting to turnbind in the other direction
+void OnPlayerRunCmd_Turnbinds(int client, int buttons, int tickcount, float angles[3])
+{
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+ if (buttons & IN_LEFT && tickcount < turnbindsLastRightStart[client] + RoundToNearest(GOKZ_TURNBIND_COOLDOWN / GetTickInterval()))
+ {
+ angles[1] = turnbindsLastValidYaw[client];
+ TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR);
+ buttons = 0;
+ }
+ else if (buttons & IN_RIGHT && tickcount < turnbindsLastLeftStart[client] + RoundToNearest(GOKZ_TURNBIND_COOLDOWN / GetTickInterval()))
+ {
+ angles[1] = turnbindsLastValidYaw[client];
+ TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR);
+ buttons = 0;
+ }
+ else
+ {
+ turnbindsLastValidYaw[client] = angles[1];
+
+ if (!(turnbindsOldButtons[client] & IN_LEFT) && (buttons & IN_LEFT))
+ {
+ turnbindsLastLeftStart[client] = tickcount;
+ }
+
+ if (!(turnbindsOldButtons[client] & IN_RIGHT) && (buttons & IN_RIGHT))
+ {
+ turnbindsLastRightStart[client] = tickcount;
+ }
+
+ turnbindsOldButtons[client] = buttons;
+ }
+}
+
+
+
+// =====[ PLAYER COLLISION ]=====
+
+void OnPlayerSpawn_PlayerCollision(int client)
+{
+ // Let players go through other players
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+}
+
+void OnSetModel_PlayerCollision(int client)
+{
+ // Fix custom models temporarily changing player collisions
+ SetEntPropVector(client, Prop_Data, "m_vecMins", PLAYER_MINS);
+ if (GetEntityFlags(client) & FL_DUCKING == 0)
+ {
+ SetEntPropVector(client, Prop_Data, "m_vecMaxs", PLAYER_MAXS);
+ }
+ else
+ {
+ SetEntPropVector(client, Prop_Data, "m_vecMaxs", PLAYER_MAXS_DUCKED);
+ }
+}
+
+
+// =====[ FORCE SV_FULL_ALLTALK 1 ]=====
+
+void OnRoundStart_ForceAllTalk()
+{
+ gCV_sv_full_alltalk.BoolValue = true;
+}
+
+
+
+// =====[ ERROR SOUNDS ]=====
+
+#define SOUND_ERROR "buttons/button10.wav"
+
+void PlayErrorSound(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_ErrorSounds) == ErrorSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, SOUND_ERROR, _, "Error");
+ }
+}
+
+
+
+// =====[ STOP SOUNDS ]=====
+
+Action OnNormalSound_StopSounds(int entity)
+{
+ char className[20];
+ GetEntityClassname(entity, className, sizeof(className));
+ if (StrEqual(className, "func_button", false))
+ {
+ return Plugin_Handled; // No sounds directly from func_button
+ }
+ return Plugin_Continue;
+}
+
+
+
+// =====[ JOIN TEAM HANDLING ]=====
+
+static bool hasSavedPosition[MAXPLAYERS + 1];
+static float savedOrigin[MAXPLAYERS + 1][3];
+static float savedAngles[MAXPLAYERS + 1][3];
+static bool savedOnLadder[MAXPLAYERS + 1];
+
+void OnClientPutInServer_JoinTeam(int client)
+{
+ // Automatically put the player on a team if he doesn't choose one.
+ // The mp_force_pick_time convar is the built in way to do this, but that obviously
+ // does not call GOKZ_JoinTeam which includes a fix for spawning in the void when
+ // there is no valid spawns available.
+ CreateTimer(12.0, Timer_ForceJoinTeam, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE);
+
+ hasSavedPosition[client] = false;
+}
+
+public Action Timer_ForceJoinTeam(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client))
+ {
+ int team = GetClientTeam(client);
+ if (team == CS_TEAM_NONE)
+ {
+ GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR, false);
+ }
+ }
+ return Plugin_Stop;
+}
+
+void OnTimerStart_JoinTeam(int client)
+{
+ hasSavedPosition[client] = false;
+}
+
+void JoinTeam(int client, int newTeam, bool restorePos, bool forceBroadcast = false)
+{
+ KZPlayer player = KZPlayer(client);
+ int currentTeam = GetClientTeam(client);
+
+ // Don't use CS_TEAM_NONE
+ if (newTeam == CS_TEAM_NONE)
+ {
+ newTeam = CS_TEAM_SPECTATOR;
+ }
+
+ if (newTeam == CS_TEAM_SPECTATOR && currentTeam != CS_TEAM_SPECTATOR)
+ {
+ if (currentTeam != CS_TEAM_NONE)
+ {
+ player.GetOrigin(savedOrigin[client]);
+ player.GetEyeAngles(savedAngles[client]);
+ savedOnLadder[client] = player.Movetype == MOVETYPE_LADDER;
+ hasSavedPosition[client] = true;
+ }
+
+ if (!player.Paused && !player.CanPause)
+ {
+ player.StopTimer();
+ }
+ ChangeClientTeam(client, CS_TEAM_SPECTATOR);
+ Call_GOKZ_OnJoinTeam(client, newTeam);
+ }
+ else if (newTeam == CS_TEAM_CT && currentTeam != CS_TEAM_CT
+ || newTeam == CS_TEAM_T && currentTeam != CS_TEAM_T)
+ {
+ ForcePlayerSuicide(client);
+ CS_SwitchTeam(client, newTeam);
+ CS_RespawnPlayer(client);
+ if (restorePos && hasSavedPosition[client])
+ {
+ TeleportPlayer(client, savedOrigin[client], savedAngles[client]);
+ if (savedOnLadder[client])
+ {
+ player.Movetype = MOVETYPE_LADDER;
+ }
+ }
+ else
+ {
+ player.StopTimer();
+ // Just joining a team alone can put you into weird invalid spawns.
+ // Need to teleport the player to a valid one.
+ float spawnOrigin[3];
+ float spawnAngles[3];
+ GetValidSpawn(spawnOrigin, spawnAngles);
+ TeleportPlayer(client, spawnOrigin, spawnAngles);
+ }
+ hasSavedPosition[client] = false;
+ Call_GOKZ_OnJoinTeam(client, newTeam);
+ }
+ else if (forceBroadcast)
+ {
+ Call_GOKZ_OnJoinTeam(client, newTeam);
+ }
+}
+
+void SendFakeTeamEvent(int client)
+{
+ // Send a fake event to close the team menu
+ Event event = CreateEvent("player_team");
+ event.SetInt("userid", GetClientUserId(client));
+ event.FireToClient(client);
+ event.Cancel();
+}
+
+// =====[ VALID JUMP TRACKING ]=====
+
+/*
+ Valid jump tracking is intended to detect when the player
+ has performed a normal jump that hasn't been affected by
+ (unexpected) teleports or other cases that may result in
+ the player becoming airborne, such as spawning.
+
+ There are ways to trick the plugin, but it is rather
+ unlikely to happen during normal gameplay.
+*/
+
+static bool validJump[MAXPLAYERS + 1];
+static float validJumpTeleportOrigin[MAXPLAYERS + 1][3];
+static int lastInvalidatedTick[MAXPLAYERS + 1];
+bool GetValidJump(int client)
+{
+ return validJump[client];
+}
+
+static void InvalidateJump(int client)
+{
+ lastInvalidatedTick[client] = GetGameTickCount();
+ if (validJump[client])
+ {
+ validJump[client] = false;
+ Call_GOKZ_OnJumpInvalidated(client);
+ }
+}
+
+void OnStopTouchGround_ValidJump(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ if (Movement_GetMovetype(client) == MOVETYPE_WALK && lastInvalidatedTick[client] != GetGameTickCount())
+ {
+ validJump[client] = true;
+ Call_GOKZ_OnJumpValidated(client, jumped, ladderJump, jumpbug);
+ }
+ else
+ {
+ InvalidateJump(client);
+ }
+}
+
+void OnPlayerRunCmdPost_ValidJump(int client)
+{
+ if (gB_VelocityTeleported[client] || gB_OriginTeleported[client])
+ {
+ InvalidateJump(client);
+ }
+}
+
+void OnChangeMovetype_ValidJump(int client, MoveType oldMovetype, MoveType newMovetype)
+{
+ if (oldMovetype == MOVETYPE_LADDER && newMovetype == MOVETYPE_WALK && lastInvalidatedTick[client] != GetGameTickCount()) // Ladderjump
+ {
+ validJump[client] = true;
+ Call_GOKZ_OnJumpValidated(client, false, true, false);
+ }
+ else
+ {
+ InvalidateJump(client);
+ }
+}
+
+void OnClientDisconnect_ValidJump(int client)
+{
+ InvalidateJump(client);
+}
+
+void OnPlayerSpawn_ValidJump(int client)
+{
+ // That should definitely be out of bounds
+ CopyVector({ 40000.0, 40000.0, 40000.0 }, validJumpTeleportOrigin[client]);
+ InvalidateJump(client);
+}
+
+void OnPlayerDeath_ValidJump(int client)
+{
+ InvalidateJump(client);
+}
+
+void OnValidOriginChange_ValidJump(int client, const float origin[3])
+{
+ CopyVector(origin, validJumpTeleportOrigin[client]);
+}
+
+void OnTeleport_ValidJump(int client)
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ if (gB_OriginTeleported[client] && GetVectorDistance(validJumpTeleportOrigin[client], origin, true) <= EPSILON)
+ {
+ gB_OriginTeleported[client] = false;
+ CopyVector({ 40000.0, 40000.0, 40000.0 }, validJumpTeleportOrigin[client]);
+ return;
+ }
+
+ if (gB_OriginTeleported[client])
+ {
+ InvalidateJump(client);
+ Call_GOKZ_OnTeleport(client);
+ }
+
+ if (gB_VelocityTeleported[client])
+ {
+ InvalidateJump(client);
+ }
+}
+
+
+
+// =====[ FIRST SPAWN ]=====
+
+static bool hasSpawned[MAXPLAYERS + 1];
+
+void OnClientPutInServer_FirstSpawn(int client)
+{
+ hasSpawned[client] = false;
+}
+
+void OnPlayerSpawn_FirstSpawn(int client)
+{
+ int team = GetClientTeam(client);
+ if (!hasSpawned[client] && (team == CS_TEAM_CT || team == CS_TEAM_T))
+ {
+ hasSpawned[client] = true;
+ Call_GOKZ_OnFirstSpawn(client);
+ }
+}
+
+
+
+// =====[ TIME LIMIT ]=====
+
+void OnConfigsExecuted_TimeLimit()
+{
+ CreateTimer(1.0, Timer_TimeLimit, _, TIMER_REPEAT);
+}
+
+public Action Timer_TimeLimit(Handle timer)
+{
+ int timelimit;
+ if (!GetMapTimeLimit(timelimit) || timelimit == 0)
+ {
+ return Plugin_Continue;
+ }
+
+ int timeleft;
+ // Check for less than -1 in case we miss 0 - ignore -1 because that means infinite time limit
+ if (GetMapTimeLeft(timeleft) && (timeleft == 0 || timeleft < -1))
+ {
+ CreateTimer(5.0, Timer_EndRound); // End the round after a delay or it won't end the map
+ return Plugin_Stop;
+ }
+
+ return Plugin_Continue;
+}
+
+public Action Timer_EndRound(Handle timer)
+{
+ CS_TerminateRound(1.0, CSRoundEnd_Draw, true);
+ return Plugin_Continue;
+}
+
+
+
+// =====[ COURSE REGISTER ]=====
+
+static bool startRegistered[GOKZ_MAX_COURSES];
+static bool endRegistered[GOKZ_MAX_COURSES];
+static bool courseRegistered[GOKZ_MAX_COURSES];
+
+bool GetCourseRegistered(int course)
+{
+ return courseRegistered[course];
+}
+
+void RegisterCourseStart(int course)
+{
+ startRegistered[course] = true;
+ TryRegisterCourse(course);
+}
+
+void RegisterCourseEnd(int course)
+{
+ endRegistered[course] = true;
+ TryRegisterCourse(course);
+}
+
+void OnMapStart_CourseRegister()
+{
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ courseRegistered[course] = false;
+ }
+}
+
+static void TryRegisterCourse(int course)
+{
+ if (!courseRegistered[course] && startRegistered[course] && endRegistered[course])
+ {
+ courseRegistered[course] = true;
+ Call_GOKZ_OnCourseRegistered(course);
+ }
+}
+
+
+
+// =====[ SPAWN FIXES ]=====
+
+void OnMapStart_FixMissingSpawns()
+{
+ int tSpawn = FindEntityByClassname(-1, "info_player_terrorist");
+ int ctSpawn = FindEntityByClassname(-1, "info_player_counterterrorist");
+
+ if (tSpawn == -1 && ctSpawn == -1)
+ {
+ LogMessage("Couldn't fix spawns because none exist.");
+ return;
+ }
+
+ if (tSpawn == -1 || ctSpawn == -1)
+ {
+ float origin[3], angles[3];
+ GetValidSpawn(origin, angles);
+
+ int newSpawn = CreateEntityByName((tSpawn == -1) ? "info_player_terrorist" : "info_player_counterterrorist");
+ if (DispatchSpawn(newSpawn))
+ {
+ TeleportEntity(newSpawn, origin, angles, NULL_VECTOR);
+ }
+ }
+}
+
+// =====[ BUTTONS ]=====
+
+void OnClientPreThinkPost_UseButtons(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_ButtonThroughPlayers) == ButtonThroughPlayers_Enabled && GetEntProp(client, Prop_Data, "m_afButtonPressed") & IN_USE)
+ {
+ int entity = FindUseEntity(client);
+ if (entity != -1)
+ {
+ AcceptEntityInput(entity, "Use", client, client, 1);
+ }
+ }
+}
+
+static int FindUseEntity(int client)
+{
+ float fwd[3];
+ float angles[3];
+ GetClientEyeAngles(client, angles);
+ GetAngleVectors(angles, fwd, NULL_VECTOR, NULL_VECTOR);
+
+ Handle trace;
+
+ float eyeOrigin[3];
+ GetClientEyePosition(client, eyeOrigin);
+ int useableContents = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE;
+
+ float endpos[3];
+
+ // Check if +use trace collide with a player first, so we don't activate any button twice
+ trace = TR_TraceRayFilterEx(eyeOrigin, angles, useableContents, RayType_Infinite, TRFOtherPlayersOnly, client);
+ if (TR_DidHit(trace))
+ {
+ int ent = TR_GetEntityIndex(trace);
+ if (ent < 1 || ent > MaxClients)
+ {
+ return -1;
+ }
+ // Search for a button behind it.
+ trace = TR_TraceRayFilterEx(eyeOrigin, angles, useableContents, RayType_Infinite, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ char buffer[20];
+ ent = TR_GetEntityIndex(trace);
+ // Make sure that it is a button, and this button activates when pressed.
+ // If it is not a button, check its parent to see if it is a button.
+ bool isButton;
+ while (ent != -1)
+ {
+ GetEntityClassname(ent, buffer, sizeof(buffer));
+ if (StrEqual("func_button", buffer, false) && GetEntProp(ent, Prop_Data, "m_spawnflags") & SF_BUTTON_USE_ACTIVATES)
+ {
+ isButton = true;
+ break;
+ }
+ else
+ {
+ ent = GetEntPropEnt(ent, Prop_Data, "m_hMoveParent");
+ }
+ }
+ if (isButton)
+ {
+ TR_GetEndPosition(endpos, trace);
+ float delta[3];
+ for (int i = 0; i < 2; i++)
+ {
+ delta[i] = endpos[i] - eyeOrigin[i];
+ }
+ // Z distance is treated differently.
+ float m_vecMins[3];
+ float m_vecMaxs[3];
+ float m_vecOrigin[3];
+ GetEntPropVector(ent, Prop_Send, "m_vecOrigin", m_vecOrigin);
+ GetEntPropVector(ent, Prop_Send, "m_vecMins", m_vecMins);
+ GetEntPropVector(ent, Prop_Send, "m_vecMaxs", m_vecMaxs);
+
+ delta[2] = IntervalDistance(endpos[2], m_vecOrigin[2] + m_vecMins[2], m_vecOrigin[2] + m_vecMaxs[2]);
+ if (GetVectorLength(delta) < 80.0)
+ {
+ return ent;
+ }
+ }
+ }
+ }
+
+ int nearestEntity;
+ float nearestPoint[3];
+ float nearestDist = FLOAT_MAX;
+ ArrayList entities = new ArrayList();
+ TR_EnumerateEntitiesSphere(eyeOrigin, 80.0, 1<<5, AddEntities, entities);
+ for (int i = 0; i < entities.Length; i++)
+ {
+ char buffer[64];
+ int ent = entities.Get(i);
+ GetEntityClassname(ent, buffer, sizeof(buffer));
+ // Check if the entity is a button and it is pressable.
+ if (StrEqual("func_button", buffer, false) && GetEntProp(ent, Prop_Data, "m_spawnflags") & SF_BUTTON_USE_ACTIVATES)
+ {
+ float point[3];
+ CalcNearestPoint(ent, eyeOrigin, point);
+
+ float dir[3];
+ for (int j = 0; j < 3; j++)
+ {
+ dir[j] = point[j] - eyeOrigin[2];
+ }
+ // Check the maximum angle the player can be away from the button.
+ float minimumDot = GetEntPropFloat(ent, Prop_Send, "m_flUseLookAtAngle");
+ NormalizeVector(dir, dir);
+ float dot = GetVectorDotProduct(dir, fwd);
+ if (dot < minimumDot)
+ {
+ continue;
+ }
+
+ float dist = CalcDistanceToLine(point, eyeOrigin, fwd);
+ if (dist < nearestDist)
+ {
+ trace = TR_TraceRayFilterEx(eyeOrigin, point, useableContents, RayType_EndPoint, TraceEntityFilterPlayers);
+ if (TR_GetFraction(trace) == 1.0 || TR_GetEntityIndex(trace) == ent)
+ {
+ CopyVector(point, nearestPoint);
+ nearestDist = dist;
+ nearestEntity = ent;
+ }
+ }
+ }
+ }
+ // We found the closest button, but we still need to check if there is a player in front of it or not.
+ // In the case that there isn't a player inbetween, we don't return the entity index, because that button will be pressed by the game function anyway.
+ // If there is, we will press two buttons at once, the "right" button found by this function and the "wrong" button that we only happen to press because
+ // there is a player in the way.
+
+ trace = TR_TraceRayFilterEx(eyeOrigin, nearestPoint, useableContents, RayType_EndPoint, TRFOtherPlayersOnly);
+ if (TR_DidHit(trace))
+ {
+ return nearestEntity;
+ }
+ return -1;
+}
+
+public bool AddEntities(int entity, ArrayList entities)
+{
+ entities.Push(entity);
+ return true;
+}
+
+static float IntervalDistance(float x, float x0, float x1)
+{
+ if (x0 > x1)
+ {
+ float tmp = x0;
+ x0 = x1;
+ x1 = tmp;
+ }
+ if (x < x0)
+ {
+ return x0 - x;
+ }
+ else if (x > x1)
+ {
+ return x - x1;
+ }
+ return 0.0;
+}
+// TraceRay filter for other players exclusively.
+public bool TRFOtherPlayersOnly(int entity, int contentmask, int client)
+{
+ return (0 < entity <= MaxClients) && (entity != client);
+}
+
+// =====[ SAFE MODE ]=====
+
+void ToggleNubSafeGuard(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledNUB)
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_Disabled);
+ }
+ else
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_EnabledNUB);
+ }
+}
+
+void ToggleProSafeGuard(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO)
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_Disabled);
+ }
+ else
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_EnabledPRO);
+ }
+}
diff --git a/sourcemod/scripting/gokz-core/modes.sp b/sourcemod/scripting/gokz-core/modes.sp
new file mode 100644
index 0000000..ce6f8ae
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/modes.sp
@@ -0,0 +1,106 @@
+static bool modeLoaded[MODE_COUNT];
+static int modeVersion[MODE_COUNT];
+static bool GOKZHitPerf[MAXPLAYERS + 1];
+static float GOKZTakeoffSpeed[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetModeLoaded(int mode)
+{
+ return modeLoaded[mode];
+}
+
+int GetModeVersion(int mode)
+{
+ return modeLoaded[mode] ? modeVersion[mode] : -1;
+}
+
+void SetModeLoaded(int mode, bool loaded, int version = -1)
+{
+ if (!modeLoaded[mode] && loaded)
+ {
+ modeLoaded[mode] = true;
+ modeVersion[mode] = version;
+ Call_GOKZ_OnModeLoaded(mode);
+ }
+ else if (modeLoaded[mode] && !loaded)
+ {
+ modeLoaded[mode] = false;
+ Call_GOKZ_OnModeUnloaded(mode);
+ }
+}
+
+int GetLoadedModeCount()
+{
+ int count = 0;
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ if (modeLoaded[mode])
+ {
+ count++;
+ }
+ }
+ return count;
+}
+
+int GetALoadedMode()
+{
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ if (GOKZ_GetModeLoaded(mode))
+ {
+ return mode;
+ }
+ }
+ return -1; // Uh-oh
+}
+
+bool GetGOKZHitPerf(int client)
+{
+ return GOKZHitPerf[client];
+}
+
+void SetGOKZHitPerf(int client, bool hitPerf)
+{
+ GOKZHitPerf[client] = hitPerf;
+}
+
+float GetGOKZTakeoffSpeed(int client)
+{
+ return GOKZTakeoffSpeed[client];
+}
+
+void SetGOKZTakeoffSpeed(int client, float takeoffSpeed)
+{
+ GOKZTakeoffSpeed[client] = takeoffSpeed;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnAllPluginsLoaded_Modes()
+{
+ if (GetLoadedModeCount() <= 0)
+ {
+ SetFailState("At least one GOKZ mode plugin is required.");
+ }
+}
+
+void OnPlayerSpawn_Modes(int client)
+{
+ GOKZHitPerf[client] = false;
+ GOKZTakeoffSpeed[client] = 0.0;
+}
+
+void OnOptionChanged_Mode(int client, Option option)
+{
+ if (option == Option_Mode)
+ {
+ // Remove speed when switching modes
+ Movement_SetVelocityModifier(client, 1.0);
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/natives.sp b/sourcemod/scripting/gokz-core/natives.sp
new file mode 100644
index 0000000..319810c
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/natives.sp
@@ -0,0 +1,647 @@
+void CreateNatives()
+{
+ CreateNative("GOKZ_GetModeLoaded", Native_GetModeLoaded);
+ CreateNative("GOKZ_GetModeVersion", Native_GetModeVersion);
+ CreateNative("GOKZ_SetModeLoaded", Native_SetModeLoaded);
+ CreateNative("GOKZ_GetLoadedModeCount", Native_GetLoadedModeCount);
+ CreateNative("GOKZ_SetMode", Native_SetMode);
+ CreateNative("GOKZ_PrintToChat", Native_PrintToChat);
+ CreateNative("GOKZ_PrintToChatAndLog", Native_PrintToChatAndLog);
+ CreateNative("GOKZ_GetOptionsTopMenu", Native_GetOptionsTopMenu);
+ CreateNative("GOKZ_GetCourseRegistered", Native_GetCourseRegistered);
+
+ CreateNative("GOKZ_StartTimer", Native_StartTimer);
+ CreateNative("GOKZ_EndTimer", Native_EndTimer);
+ CreateNative("GOKZ_StopTimer", Native_StopTimer);
+ CreateNative("GOKZ_StopTimerAll", Native_StopTimerAll);
+ CreateNative("GOKZ_TeleportToStart", Native_TeleportToStart);
+ CreateNative("GOKZ_TeleportToSearchStart", Native_TeleportToSearchStart);
+ CreateNative("GOKZ_GetVirtualButtonPosition", Native_GetVirtualButtonPosition);
+ CreateNative("GOKZ_SetVirtualButtonPosition", Native_SetVirtualButtonPosition);
+ CreateNative("GOKZ_ResetVirtualButtonPosition", Native_ResetVirtualButtonPosition);
+ CreateNative("GOKZ_LockVirtualButtons", Native_LockVirtualButtons);
+ CreateNative("GOKZ_GetStartPosition", Native_GetStartPosition);
+ CreateNative("GOKZ_SetStartPosition", Native_SetStartPosition);
+ CreateNative("GOKZ_TeleportToEnd", Native_TeleportToEnd);
+ CreateNative("GOKZ_GetStartPositionType", Native_GetStartPositionType);
+ CreateNative("GOKZ_SetStartPositionToMapStart", Native_SetStartPositionToMapStart);
+ CreateNative("GOKZ_MakeCheckpoint", Native_MakeCheckpoint);
+ CreateNative("GOKZ_GetCanMakeCheckpoint", Native_GetCanMakeCheckpoint);
+ CreateNative("GOKZ_TeleportToCheckpoint", Native_TeleportToCheckpoint);
+ CreateNative("GOKZ_GetCanTeleportToCheckpoint", Native_GetCanTeleportToCheckpoint);
+ CreateNative("GOKZ_PrevCheckpoint", Native_PrevCheckpoint);
+ CreateNative("GOKZ_GetCanPrevCheckpoint", Native_GetCanPrevCheckpoint);
+ CreateNative("GOKZ_NextCheckpoint", Native_NextCheckpoint);
+ CreateNative("GOKZ_GetCanNextCheckpoint", Native_GetCanNextCheckpoint);
+ CreateNative("GOKZ_UndoTeleport", Native_UndoTeleport);
+ CreateNative("GOKZ_GetCanUndoTeleport", Native_GetCanUndoTeleport);
+ CreateNative("GOKZ_Pause", Native_Pause);
+ CreateNative("GOKZ_GetCanPause", Native_GetCanPause);
+ CreateNative("GOKZ_Resume", Native_Resume);
+ CreateNative("GOKZ_GetCanResume", Native_GetCanResume);
+ CreateNative("GOKZ_TogglePause", Native_TogglePause);
+ CreateNative("GOKZ_GetCanTeleportToStart", Native_GetCanTeleportToStart);
+ CreateNative("GOKZ_GetCanTeleportToEnd", Native_GetCanTeleportToEnd);
+ CreateNative("GOKZ_PlayErrorSound", Native_PlayErrorSound);
+ CreateNative("GOKZ_SetValidJumpOrigin", Native_SetValidJumpOrigin);
+
+ CreateNative("GOKZ_GetTimerRunning", Native_GetTimerRunning);
+ CreateNative("GOKZ_GetValidTimer", Native_GetValidTimer);
+ CreateNative("GOKZ_GetCourse", Native_GetCourse);
+ CreateNative("GOKZ_SetCourse", Native_SetCourse);
+ CreateNative("GOKZ_GetPaused", Native_GetPaused);
+ CreateNative("GOKZ_GetTime", Native_GetTime);
+ CreateNative("GOKZ_SetTime", Native_SetTime);
+ CreateNative("GOKZ_InvalidateRun", Native_InvalidateRun);
+ CreateNative("GOKZ_GetCheckpointCount", Native_GetCheckpointCount);
+ CreateNative("GOKZ_SetCheckpointCount", Native_SetCheckpointCount);
+ CreateNative("GOKZ_GetCheckpointData", Native_GetCheckpointData);
+ CreateNative("GOKZ_SetCheckpointData", Native_SetCheckpointData);
+ CreateNative("GOKZ_GetUndoTeleportData", Native_GetUndoTeleportData);
+ CreateNative("GOKZ_SetUndoTeleportData", Native_SetUndoTeleportData);
+ CreateNative("GOKZ_GetTeleportCount", Native_GetTeleportCount);
+ CreateNative("GOKZ_SetTeleportCount", Native_SetTeleportCount);
+ CreateNative("GOKZ_RegisterOption", Native_RegisterOption);
+ CreateNative("GOKZ_GetOptionProp", Native_GetOptionProp);
+ CreateNative("GOKZ_SetOptionProp", Native_SetOptionProp);
+ CreateNative("GOKZ_GetOption", Native_GetOption);
+ CreateNative("GOKZ_SetOption", Native_SetOption);
+ CreateNative("GOKZ_GetHitPerf", Native_GetHitPerf);
+ CreateNative("GOKZ_SetHitPerf", Native_SetHitPerf);
+ CreateNative("GOKZ_GetTakeoffSpeed", Native_GetTakeoffSpeed);
+ CreateNative("GOKZ_SetTakeoffSpeed", Native_SetTakeoffSpeed);
+ CreateNative("GOKZ_GetValidJump", Native_GetValidJump);
+ CreateNative("GOKZ_JoinTeam", Native_JoinTeam);
+
+ CreateNative("GOKZ_EmitSoundToClient", Native_EmitSoundToClient);
+}
+
+public int Native_GetModeLoaded(Handle plugin, int numParams)
+{
+ return view_as<int>(GetModeLoaded(GetNativeCell(1)));
+}
+
+public int Native_GetModeVersion(Handle plugin, int numParams)
+{
+ return view_as<int>(GetModeVersion(GetNativeCell(1)));
+}
+
+public int Native_SetModeLoaded(Handle plugin, int numParams)
+{
+ SetModeLoaded(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3));
+ return 0;
+}
+
+public int Native_GetLoadedModeCount(Handle plugin, int numParams)
+{
+ return GetLoadedModeCount();
+}
+
+public int Native_SetMode(Handle plugin, int numParams)
+{
+ return view_as<bool>(SwitchToModeIfAvailable(GetNativeCell(1),GetNativeCell(2)));
+}
+
+public int Native_PrintToChatAndLog(Handle plugin, int numParams)
+{
+ NativeHelper_PrintToChatOrLog(true);
+ return 0;
+}
+
+public int Native_PrintToChat(Handle plugin, int numParams)
+{
+ NativeHelper_PrintToChatOrLog(false);
+ return 0;
+}
+
+static int NativeHelper_PrintToChatOrLog(bool alwaysLog)
+{
+ int client = GetNativeCell(1);
+ bool addPrefix = GetNativeCell(2);
+
+ char buffer[1024];
+ SetGlobalTransTarget(client);
+ FormatNativeString(0, 3, 4, sizeof(buffer), _, buffer);
+
+ // The console (client 0) gets a special treatment
+ if (client == 0 || (!IsValidClient(client) && !IsClientSourceTV(client)) || alwaysLog)
+ {
+ // Strip colors
+ // We can't regex-replace, so I'm quite sure that's the most efficient way.
+ // It's also not perfectly safe, we will just assume you never have curly
+ // braces without a color in beween.
+ char colorlessBuffer[1024];
+ FormatEx(colorlessBuffer, sizeof(colorlessBuffer), "%L: ", client);
+ int iIn = 0, iOut = strlen(colorlessBuffer);
+ do
+ {
+ if (buffer[iIn] == '{')
+ {
+ for (; buffer[iIn] != '}' && iIn < sizeof(buffer) - 2; iIn++){}
+ if (iIn >= sizeof(buffer) - 2)
+ {
+ break;
+ }
+ iIn++;
+ continue;
+ }
+
+ colorlessBuffer[iOut] = buffer[iIn];
+ iIn++;
+ iOut++;
+ } while (buffer[iIn] != '\0' && iIn < sizeof(buffer) - 1 && iOut < sizeof(colorlessBuffer) - 1);
+ colorlessBuffer[iOut] = '\0';
+ LogMessage(colorlessBuffer);
+ }
+
+ if (client != 0)
+ {
+ if (addPrefix)
+ {
+ char prefix[64];
+ gCV_gokz_chat_prefix.GetString(prefix, sizeof(prefix));
+ Format(buffer, sizeof(buffer), "%s%s", prefix, buffer);
+ }
+
+ CPrintToChat(client, "%s", buffer);
+ }
+ return 0;
+}
+
+public int Native_GetOptionsTopMenu(Handle plugin, int numParams)
+{
+ return view_as<int>(GetOptionsTopMenu());
+}
+
+public int Native_GetCourseRegistered(Handle plugin, int numParams)
+{
+ return view_as<int>(GetCourseRegistered(GetNativeCell(1)));
+}
+
+public int Native_StartTimer(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ return view_as<int>(TimerStart(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3)));
+}
+
+public int Native_EndTimer(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ return view_as<int>(TimerEnd(GetNativeCell(1), GetNativeCell(2)));
+}
+
+public int Native_StopTimer(Handle plugin, int numParams)
+{
+ return view_as<int>(TimerStop(GetNativeCell(1), GetNativeCell(2)));
+}
+
+public int Native_StopTimerAll(Handle plugin, int numParams)
+{
+ TimerStopAll(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_TeleportToStart(Handle plugin, int numParams)
+{
+ TeleportToStart(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_TeleportToSearchStart(Handle plugin, int numParams)
+{
+ TeleportToSearchStart(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
+
+public int Native_GetVirtualButtonPosition(Handle plugin, int numParams)
+{
+ int course;
+ float position[3];
+
+ course = GetVirtualButtonPosition(GetNativeCell(1), position, GetNativeCell(3));
+ SetNativeArray(2, position, sizeof(position));
+
+ return course;
+}
+
+public int Native_SetVirtualButtonPosition(Handle plugin, int numParams)
+{
+ float position[3];
+
+ GetNativeArray(2, position, sizeof(position));
+ SetVirtualButtonPosition(GetNativeCell(1), position, GetNativeCell(3), view_as<bool>(GetNativeCell(4)));
+ return 0;
+}
+
+public int Native_ResetVirtualButtonPosition(Handle plugin, int numParams)
+{
+ ResetVirtualButtonPosition(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
+
+public int Native_LockVirtualButtons(Handle plugin, int numParams)
+{
+ LockVirtualButtons(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetStartPosition(Handle plugin, int numParams)
+{
+ StartPositionType type;
+ float position[3], angles[3];
+
+ type = GetStartPosition(GetNativeCell(1), position, angles);
+ SetNativeArray(2, position, sizeof(position));
+ SetNativeArray(3, angles, sizeof(angles));
+
+ return view_as<int>(type);
+}
+
+public int Native_SetStartPosition(Handle plugin, int numParams)
+{
+ float position[3], angles[3];
+
+ GetNativeArray(3, position, sizeof(position));
+ GetNativeArray(4, angles, sizeof(angles));
+ SetStartPosition(GetNativeCell(1), GetNativeCell(2), position, angles);
+ return 0;
+}
+
+public int Native_TeleportToEnd(Handle plugin, int numParams)
+{
+ TeleportToEnd(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
+
+public int Native_GetStartPositionType(Handle plugin, int numParams)
+{
+ return view_as<int>(GetStartPositionType(GetNativeCell(1)));
+}
+
+public int Native_SetStartPositionToMapStart(Handle plugin, int numParams)
+{
+ return SetStartPositionToMapStart(GetNativeCell(1), GetNativeCell(2));
+}
+
+public int Native_MakeCheckpoint(Handle plugin, int numParams)
+{
+ MakeCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanMakeCheckpoint(Handle plugin, int numParams)
+{
+ return CanMakeCheckpoint(GetNativeCell(1));
+}
+
+public int Native_TeleportToCheckpoint(Handle plugin, int numParams)
+{
+ TeleportToCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanTeleportToCheckpoint(Handle plugin, int numParams)
+{
+ return CanTeleportToCheckpoint(GetNativeCell(1));
+}
+
+public int Native_PrevCheckpoint(Handle plugin, int numParams)
+{
+ PrevCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanPrevCheckpoint(Handle plugin, int numParams)
+{
+ return CanPrevCheckpoint(GetNativeCell(1));
+}
+
+public int Native_NextCheckpoint(Handle plugin, int numParams)
+{
+ NextCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanNextCheckpoint(Handle plugin, int numParams)
+{
+ return CanNextCheckpoint(GetNativeCell(1));
+}
+
+public int Native_UndoTeleport(Handle plugin, int numParams)
+{
+ UndoTeleport(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanUndoTeleport(Handle plugin, int numParams)
+{
+ return CanUndoTeleport(GetNativeCell(1));
+}
+
+public int Native_Pause(Handle plugin, int numParams)
+{
+ Pause(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanPause(Handle plugin, int numParams)
+{
+ return CanPause(GetNativeCell(1));
+}
+
+public int Native_Resume(Handle plugin, int numParams)
+{
+ Resume(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanResume(Handle plugin, int numParams)
+{
+ return CanResume(GetNativeCell(1));
+}
+
+public int Native_TogglePause(Handle plugin, int numParams)
+{
+ TogglePause(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanTeleportToStart(Handle plugin, int numParams)
+{
+ return CanTeleportToStart(GetNativeCell(1));
+}
+
+public int Native_GetCanTeleportToEnd(Handle plugin, int numParams)
+{
+ return CanTeleportToEnd(GetNativeCell(1));
+}
+
+public int Native_PlayErrorSound(Handle plugin, int numParams)
+{
+ PlayErrorSound(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_SetValidJumpOrigin(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ float origin[3];
+ GetNativeArray(2, origin, sizeof(origin));
+
+ // The order is important here!
+ OnValidOriginChange_ValidJump(client, origin);
+
+ // Using Movement_SetOrigin instead causes considerable lag for spectators
+ SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+ return 0;
+}
+
+public int Native_GetTimerRunning(Handle plugin, int numParams)
+{
+ return view_as<int>(GetTimerRunning(GetNativeCell(1)));
+}
+
+public int Native_GetValidTimer(Handle plugin, int numParams)
+{
+ return view_as<int>(GetValidTimer(GetNativeCell(1)));
+}
+
+public int Native_GetCourse(Handle plugin, int numParams)
+{
+ return GetCurrentCourse(GetNativeCell(1));
+}
+
+public int Native_SetCourse(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ SetCurrentCourse(GetNativeCell(1), GetNativeCell(2));
+ return view_as<int>(false);
+}
+
+public int Native_GetPaused(Handle plugin, int numParams)
+{
+ return view_as<int>(GetPaused(GetNativeCell(1)));
+}
+
+public int Native_GetTime(Handle plugin, int numParams)
+{
+ return view_as<int>(GetCurrentTime(GetNativeCell(1)));
+}
+
+public int Native_SetTime(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ SetCurrentTime(GetNativeCell(1), view_as<float>(GetNativeCell(2)));
+ return view_as<int>(true);
+}
+
+public int Native_InvalidateRun(Handle plugin, int numParams)
+{
+ InvalidateRun(GetNativeCell(1));
+ return view_as<int>(true);
+}
+
+public int Native_GetCheckpointCount(Handle plugin, int numParams)
+{
+ return GetCheckpointCount(GetNativeCell(1));
+}
+
+public int Native_SetCheckpointCount(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ SetCheckpointCount(GetNativeCell(1), GetNativeCell(2));
+ return view_as<int>(true);
+}
+
+public int Native_GetCheckpointData(Handle plugin, int numParams)
+{
+ ArrayList temp = GetCheckpointData(GetNativeCell(1));
+ Handle cps = CloneHandle(temp, plugin);
+ delete temp;
+ return view_as<int>(cps);
+}
+
+public int Native_SetCheckpointData(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ return SetCheckpointData(GetNativeCell(1), view_as<ArrayList>(GetNativeCell(2)), GetNativeCell(3));
+}
+
+public int Native_GetUndoTeleportData(Handle plugin, int numParams)
+{
+ ArrayList temp = GetUndoTeleportData(GetNativeCell(1));
+ Handle utd = CloneHandle(temp, plugin);
+ delete temp;
+ return view_as<int>(utd);
+}
+
+public int Native_SetUndoTeleportData(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ return SetUndoTeleportData(GetNativeCell(1), view_as<ArrayList>(GetNativeCell(2)), GetNativeCell(3));
+}
+
+public int Native_GetTeleportCount(Handle plugin, int numParams)
+{
+ return GetTeleportCount(GetNativeCell(1));
+}
+
+public int Native_SetTeleportCount(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ SetTeleportCount(GetNativeCell(1), GetNativeCell(2));
+ return view_as<int>(true);
+}
+
+public int Native_RegisterOption(Handle plugin, int numParams)
+{
+ char name[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(1, name, sizeof(name));
+ char description[255];
+ GetNativeString(2, description, sizeof(description));
+ return view_as<int>(RegisterOption(name, description, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), GetNativeCell(6)));
+}
+
+public int Native_GetOptionProp(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(1, option, sizeof(option));
+ OptionProp prop = GetNativeCell(2);
+ any value = GetOptionProp(option, prop);
+
+ // Return clone of Handle if called by another plugin
+ if (prop == OptionProp_Cookie && plugin != gH_ThisPlugin)
+ {
+ value = CloneHandle(value, plugin);
+ }
+
+ return value;
+}
+
+public int Native_SetOptionProp(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(1, option, sizeof(option));
+ OptionProp prop = GetNativeCell(2);
+ return SetOptionProp(option, prop, GetNativeCell(3));
+}
+
+public int Native_GetOption(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(2, option, sizeof(option));
+ return view_as<int>(GetOption(GetNativeCell(1), option));
+}
+
+public int Native_SetOption(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(2, option, sizeof(option));
+ return view_as<int>(SetOption(GetNativeCell(1), option, GetNativeCell(3)));
+}
+
+public int Native_GetHitPerf(Handle plugin, int numParams)
+{
+ return view_as<int>(GetGOKZHitPerf(GetNativeCell(1)));
+}
+
+public int Native_SetHitPerf(Handle plugin, int numParams)
+{
+ SetGOKZHitPerf(GetNativeCell(1), view_as<bool>(GetNativeCell(2)));
+ return 0;
+}
+
+public int Native_GetTakeoffSpeed(Handle plugin, int numParams)
+{
+ return view_as<int>(GetGOKZTakeoffSpeed(GetNativeCell(1)));
+}
+
+public int Native_SetTakeoffSpeed(Handle plugin, int numParams)
+{
+ SetGOKZTakeoffSpeed(GetNativeCell(1), view_as<float>(GetNativeCell(2)));
+ return 0;
+}
+
+public int Native_GetValidJump(Handle plugin, int numParams)
+{
+ return view_as<int>(GetValidJump(GetNativeCell(1)));
+}
+
+public int Native_JoinTeam(Handle plugin, int numParams)
+{
+ JoinTeam(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3), GetNativeCell(4));
+ return 0;
+}
+
+public int Native_EmitSoundToClient(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+
+ char sample[PLATFORM_MAX_PATH];
+ GetNativeString(2, sample, sizeof(sample));
+
+ float volume = GetNativeCell(3);
+ float newVolume = volume;
+
+ char description[64];
+ GetNativeString(4, description, sizeof(description));
+
+ Action result;
+
+ Call_GOKZ_OnEmitSoundToClient(client, sample, newVolume, description, result);
+ if (result == Plugin_Stop)
+ {
+ return 0;
+ }
+ if (result == Plugin_Changed)
+ {
+ EmitSoundToClient(client, sample, _, _, _, _, newVolume);
+ return 0;
+ }
+ EmitSoundToClient(client, sample, _, _, _, _, volume);
+ return 0;
+}
+
+// =====[ PRIVATE ]=====
+
+static bool BlockedExternallyCalledTimerNative(Handle plugin, int client)
+{
+ if (plugin != gH_ThisPlugin)
+ {
+ Action result;
+ Call_GOKZ_OnTimerNativeCalledExternally(plugin, client, result);
+ if (result != Plugin_Continue)
+ {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/sourcemod/scripting/gokz-core/options.sp b/sourcemod/scripting/gokz-core/options.sp
new file mode 100644
index 0000000..6daef13
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/options.sp
@@ -0,0 +1,438 @@
+static StringMap optionData;
+static StringMap optionDescriptions;
+
+
+
+// =====[ PUBLIC ]=====
+
+bool RegisterOption(const char[] name, const char[] description, OptionType type, any defaultValue, any minValue, any maxValue)
+{
+ if (!IsValueInRange(type, defaultValue, minValue, maxValue))
+ {
+ LogError("Failed to register option \"%s\" due to invalid default value and value range.", name);
+ return false;
+ }
+
+ if (strlen(name) > GOKZ_OPTION_MAX_NAME_LENGTH - 1)
+ {
+ LogError("Failed to register option \"%s\" because its name is too long.", name);
+ return false;
+ }
+
+ if (strlen(name) > GOKZ_OPTION_MAX_NAME_LENGTH - 1)
+ {
+ LogError("Failed to register option \"%s\" because its description is too long.", name);
+ return false;
+ }
+
+ ArrayList data;
+ Cookie cookie;
+ if (IsRegisteredOption(name))
+ {
+ optionData.GetValue(name, data);
+ cookie = GetOptionProp(name, OptionProp_Cookie);
+ }
+ else
+ {
+ data = new ArrayList(1, view_as<int>(OPTIONPROP_COUNT));
+ cookie = new Cookie(name, description, CookieAccess_Private);
+ }
+
+ data.Set(view_as<int>(OptionProp_Cookie), cookie);
+ data.Set(view_as<int>(OptionProp_Type), type);
+ data.Set(view_as<int>(OptionProp_DefaultValue), defaultValue);
+ data.Set(view_as<int>(OptionProp_MinValue), minValue);
+ data.Set(view_as<int>(OptionProp_MaxValue), maxValue);
+
+ optionData.SetValue(name, data, true);
+ optionDescriptions.SetString(name, description, true);
+
+ // Support late-loading/registering
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (AreClientCookiesCached(client))
+ {
+ LoadOption(client, name);
+ }
+ }
+
+ return true;
+}
+
+any GetOptionProp(const char[] option, OptionProp prop)
+{
+ ArrayList data;
+ if (!optionData.GetValue(option, data))
+ {
+ LogError("Failed to get option property of unregistered option \"%s\".", option);
+ return -1;
+ }
+
+ return data.Get(view_as<int>(prop));
+}
+
+bool SetOptionProp(const char[] option, OptionProp prop, any newValue)
+{
+ ArrayList data;
+ if (!optionData.GetValue(option, data))
+ {
+ LogError("Failed to set property of unregistered option \"%s\".", option);
+ return false;
+ }
+
+ if (prop == OptionProp_Cookie)
+ {
+ LogError("Failed to set cookie of option \"%s\" as it is read-only.");
+ return false;
+ }
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ any defaultValue = GetOptionProp(option, OptionProp_DefaultValue);
+ any minValue = GetOptionProp(option, OptionProp_MinValue);
+ any maxValue = GetOptionProp(option, OptionProp_MaxValue);
+
+ switch (prop)
+ {
+ case OptionProp_DefaultValue:
+ {
+ if (!IsValueInRange(type, newValue, minValue, maxValue))
+ {
+ LogError("Failed to set default value of option \"%s\" due to invalid default value and value range.", option);
+ return false;
+ }
+ }
+ case OptionProp_MinValue:
+ {
+ if (!IsValueInRange(type, defaultValue, newValue, maxValue))
+ {
+ LogError("Failed to set minimum value of option \"%s\" due to invalid default value and value range.", option);
+ return false;
+ }
+ }
+ case OptionProp_MaxValue:
+ {
+ if (!IsValueInRange(type, defaultValue, minValue, newValue))
+ {
+ LogError("Failed to set maximum value of option \"%s\" due to invalid default value and value range.", option);
+ return false;
+ }
+ }
+ }
+
+ data.Set(view_as<int>(prop), newValue);
+ return optionData.SetValue(option, data, true);
+}
+
+any GetOption(int client, const char[] option)
+{
+ if (!IsRegisteredOption(option))
+ {
+ LogError("Failed to get value of unregistered option \"%s\".", option);
+ return -1;
+ }
+
+ Cookie cookie = GetOptionProp(option, OptionProp_Cookie);
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ char value[100];
+ cookie.Get(client, value, sizeof(value));
+
+ if (type == OptionType_Float)
+ {
+ return StringToFloat(value);
+ }
+ else //if (type == OptionType_Int)
+ {
+ return StringToInt(value);
+ }
+}
+
+bool SetOption(int client, const char[] option, any newValue)
+{
+ if (!IsRegisteredOption(option))
+ {
+ LogError("Failed to set value of unregistered option \"%s\".", option);
+ return false;
+ }
+
+ if (GetOption(client, option) == newValue)
+ {
+ return true;
+ }
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ any minValue = GetOptionProp(option, OptionProp_MinValue);
+ any maxValue = GetOptionProp(option, OptionProp_MaxValue);
+
+ if (!IsValueInRange(type, newValue, minValue, maxValue))
+ {
+ LogError("Failed to set value of option \"%s\" because desired value was outside registered value range.", option);
+ return false;
+ }
+
+ char newValueString[100];
+ if (type == OptionType_Float)
+ {
+ FloatToString(newValue, newValueString, sizeof(newValueString));
+ }
+ else //if (type == OptionType_Int)
+ {
+ IntToString(newValue, newValueString, sizeof(newValueString));
+ }
+
+ Cookie cookie = GetOptionProp(option, OptionProp_Cookie);
+ cookie.Set(client, newValueString);
+
+ if (IsClientInGame(client))
+ {
+ Call_GOKZ_OnOptionChanged(client, option, newValue);
+ }
+
+ return true;
+}
+
+bool IsRegisteredOption(const char[] option)
+{
+ int dummy;
+ return optionData.GetValue(option, dummy);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_Options()
+{
+ optionData = new StringMap();
+ optionDescriptions = new StringMap();
+ RegisterOptions();
+}
+
+void OnClientCookiesCached_Options(int client)
+{
+ StringMapSnapshot optionDataSnapshot = optionData.Snapshot();
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+
+ for (int i = 0; i < optionDataSnapshot.Length; i++)
+ {
+ optionDataSnapshot.GetKey(i, option, sizeof(option));
+ LoadOption(client, option);
+ }
+
+ delete optionDataSnapshot;
+
+ Call_GOKZ_OnOptionsLoaded(client);
+}
+
+void OnClientPutInServer_Options(int client)
+{
+ if (!GetModeLoaded(GOKZ_GetCoreOption(client, Option_Mode)))
+ {
+ GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode());
+ }
+}
+
+void OnOptionChanged_Options(int client, Option option, int newValue)
+{
+ if (option == Option_Mode && !GetModeLoaded(newValue))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Mode Not Available", newValue);
+ GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode());
+ }
+ else
+ {
+ PrintOptionChangeMessage(client, option, newValue);
+ }
+}
+
+void OnModeUnloaded_Options(int mode)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && GOKZ_GetCoreOption(client, Option_Mode) == mode)
+ {
+ GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode());
+ }
+ }
+}
+
+void OnMapStart_Options()
+{
+ LoadDefaultOptions();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void RegisterOptions()
+{
+ for (Option option; option < OPTION_COUNT; option++)
+ {
+ RegisterOption(gC_CoreOptionNames[option], gC_CoreOptionDescriptions[option],
+ OptionType_Int, gI_CoreOptionDefaults[option], 0, gI_CoreOptionCounts[option] - 1);
+ }
+}
+
+static bool IsValueInRange(OptionType type, any value, any minValue, any maxValue)
+{
+ if (type == OptionType_Float)
+ {
+ return FloatCompare(minValue, value) <= 0 && FloatCompare(value, maxValue) <= 0;
+ }
+ else //if (type == OptionType_Int)
+ {
+ return minValue <= value && value <= maxValue;
+ }
+}
+
+static void LoadOption(int client, const char[] option)
+{
+ char valueString[100];
+ Cookie cookie = GetOptionProp(option, OptionProp_Cookie);
+ cookie.Get(client, valueString, sizeof(valueString));
+
+ // If there's no stored value for the option, set it to default
+ if (valueString[0] == '\0')
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ return;
+ }
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ any minValue = GetOptionProp(option, OptionProp_MinValue);
+ any maxValue = GetOptionProp(option, OptionProp_MaxValue);
+ any value;
+
+ // If stored option isn't a valid float or integer, or is out of range, set it to default
+ if (type == OptionType_Float && StringToFloatEx(valueString, value) == 0)
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ else if (type == OptionType_Int && StringToIntEx(valueString, value) == 0)
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ else if (!IsValueInRange(type, value, minValue, maxValue))
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+}
+
+// Load default optionData from a config file, creating one and adding optionData if necessary
+static void LoadDefaultOptions()
+{
+ KeyValues oldKV = new KeyValues(GOKZ_CFG_OPTIONS_ROOT);
+
+ if (FileExists(GOKZ_CFG_OPTIONS) && !oldKV.ImportFromFile(GOKZ_CFG_OPTIONS))
+ {
+ LogError("Failed to load file: \"%s\".", GOKZ_CFG_OPTIONS);
+ delete oldKV;
+ return;
+ }
+
+ KeyValues newKV = new KeyValues(GOKZ_CFG_OPTIONS_ROOT); // This one will be sorted by option name
+ StringMapSnapshot optionDataSnapshot = optionData.Snapshot();
+ ArrayList optionDataSnapshotArray = new ArrayList(ByteCountToCells(GOKZ_OPTION_MAX_NAME_LENGTH), 0);
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ char optionDescription[GOKZ_OPTION_MAX_DESC_LENGTH];
+
+ // Sort the optionData by name
+ for (int i = 0; i < optionDataSnapshot.Length; i++)
+ {
+ optionDataSnapshot.GetKey(i, option, sizeof(option));
+ optionDataSnapshotArray.PushString(option);
+ }
+ SortADTArray(optionDataSnapshotArray, Sort_Ascending, Sort_String);
+
+ // Get the values from the KeyValues, otherwise set them
+ for (int i = 0; i < optionDataSnapshotArray.Length; i++)
+ {
+ oldKV.Rewind();
+ newKV.Rewind();
+ optionDataSnapshotArray.GetString(i, option, sizeof(option));
+ optionDescriptions.GetString(option, optionDescription, sizeof(optionDescription));
+
+ newKV.JumpToKey(option, true);
+ newKV.SetString(GOKZ_CFG_OPTIONS_DESCRIPTION, optionDescription);
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ if (type == OptionType_Float)
+ {
+ if (oldKV.JumpToKey(option, false) && oldKV.JumpToKey(GOKZ_CFG_OPTIONS_DEFAULT, false))
+ {
+ oldKV.GoBack();
+ newKV.SetFloat(GOKZ_CFG_OPTIONS_DEFAULT, oldKV.GetFloat(GOKZ_CFG_OPTIONS_DEFAULT));
+ SetOptionProp(option, OptionProp_DefaultValue, oldKV.GetFloat(GOKZ_CFG_OPTIONS_DEFAULT));
+ }
+ else
+ {
+ newKV.SetFloat(GOKZ_CFG_OPTIONS_DEFAULT, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ }
+ else if (type == OptionType_Int)
+ {
+ if (oldKV.JumpToKey(option, false) && oldKV.JumpToKey(GOKZ_CFG_OPTIONS_DEFAULT, false))
+ {
+ oldKV.GoBack();
+ newKV.SetNum(GOKZ_CFG_OPTIONS_DEFAULT, oldKV.GetNum(GOKZ_CFG_OPTIONS_DEFAULT));
+ SetOptionProp(option, OptionProp_DefaultValue, oldKV.GetNum(GOKZ_CFG_OPTIONS_DEFAULT));
+ }
+ else
+ {
+ newKV.SetNum(GOKZ_CFG_OPTIONS_DEFAULT, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ }
+ }
+
+ newKV.Rewind();
+ newKV.ExportToFile(GOKZ_CFG_OPTIONS);
+
+ delete oldKV;
+ delete newKV;
+ delete optionDataSnapshot;
+ delete optionDataSnapshotArray;
+}
+
+static void PrintOptionChangeMessage(int client, Option option, int newValue)
+{
+ // NOTE: Not all optionData have a message for when they are changed.
+ switch (option)
+ {
+ case Option_Mode:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Switched Mode", gC_ModeNames[newValue]);
+ }
+ case Option_VirtualButtonIndicators:
+ {
+ switch (newValue)
+ {
+ case VirtualButtonIndicators_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Virtual Button Indicators - Disable");
+ }
+ case VirtualButtonIndicators_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Virtual Button Indicators - Enable");
+ }
+ }
+ }
+ case Option_Safeguard:
+ {
+ switch (newValue)
+ {
+ case Safeguard_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Disable");
+ }
+ case Safeguard_EnabledNUB:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Enable (NUB)");
+ }
+ case Safeguard_EnabledPRO:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Enable (PRO)");
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/teamnumfix.sp b/sourcemod/scripting/gokz-core/teamnumfix.sp
new file mode 100644
index 0000000..0c5d81d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/teamnumfix.sp
@@ -0,0 +1,68 @@
+static Handle H_RemovePlayer;
+static int teamEntID[4];
+static int oldTeam[MAXPLAYERS + 1];
+static int realTeam[MAXPLAYERS + 1];
+
+void OnPluginStart_TeamNumber()
+{
+ GameData gamedataConf = LoadGameConfigFile("gokz-core.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-core gamedata");
+ }
+
+ StartPrepSDKCall(SDKCall_Entity);
+ PrepSDKCall_SetVirtual(gamedataConf.GetOffset("CCSTeam::RemovePlayer"));
+ PrepSDKCall_AddParameter(SDKType_CBasePlayer, SDKPass_Pointer);
+ H_RemovePlayer = EndPrepSDKCall();
+ if (H_RemovePlayer == INVALID_HANDLE)
+ {
+ SetFailState("Unable to prepare SDKCall for CCSTeam::RemovePlayer!");
+ }
+}
+
+void OnMapStart_TeamNumber()
+{
+ // Fetch the entity ID of team entities and store them.
+ int team = FindEntityByClassname(MaxClients + 1, "cs_team_manager");
+ while (team != -1)
+ {
+ int teamNum = GetEntProp(team, Prop_Send, "m_iTeamNum");
+ teamEntID[teamNum] = team;
+ team = FindEntityByClassname(team, "cs_team_manager");
+ }
+}
+
+void OnGameFrame_TeamNumber()
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (!IsClientInGame(client) || !IsPlayerAlive(client))
+ {
+ continue;
+ }
+ int team = GetEntProp(client, Prop_Data, "m_iTeamNum");
+ // If the entprop changed, remove the player from the old team, but make sure it's a valid team first
+ if (team != oldTeam[client] && oldTeam[client] < 4 && oldTeam[client] > 0)
+ {
+ SDKCall(H_RemovePlayer, teamEntID[oldTeam[client]], client);
+ }
+ oldTeam[client] = team;
+ }
+}
+
+void OnPlayerJoinTeam_TeamNumber(Event event, int client)
+{
+ // If the old team value is invalid, fix it.
+ if (event.GetInt("oldteam") > 4 || event.GetInt("oldteam") < 0)
+ {
+ event.SetInt("oldteam", 0);
+ }
+ realTeam[client] = event.GetInt("team");
+}
+
+void OnPlayerDeath_TeamNumber(int client)
+{
+ // Switch the client's team to a valid team to prevent crashes.
+ CS_SwitchTeam(client, realTeam[client]);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/teleports.sp b/sourcemod/scripting/gokz-core/teleports.sp
new file mode 100644
index 0000000..764fc6e
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/teleports.sp
@@ -0,0 +1,917 @@
+/*
+ Checkpoints and teleporting, including ability to go back
+ to previous checkpoint, go to next checkpoint, and undo.
+*/
+
+
+static ArrayList checkpoints[MAXPLAYERS + 1];
+static int checkpointCount[MAXPLAYERS + 1];
+static int checkpointIndex[MAXPLAYERS + 1];
+static int checkpointIndexStart[MAXPLAYERS + 1];
+static int checkpointIndexEnd[MAXPLAYERS + 1];
+static int teleportCount[MAXPLAYERS + 1];
+static StartPositionType startType[MAXPLAYERS + 1];
+static StartPositionType nonCustomStartType[MAXPLAYERS + 1];
+static float nonCustomStartOrigin[MAXPLAYERS + 1][3];
+static float nonCustomStartAngles[MAXPLAYERS + 1][3];
+static float customStartOrigin[MAXPLAYERS + 1][3];
+static float customStartAngles[MAXPLAYERS + 1][3];
+static float endOrigin[MAXPLAYERS + 1][3];
+static float endAngles[MAXPLAYERS + 1][3];
+static UndoTeleportData undoTeleportData[MAXPLAYERS + 1];
+static float lastRestartAttemptTime[MAXPLAYERS + 1];
+
+// =====[ PUBLIC ]=====
+
+int GetCheckpointCount(int client)
+{
+ return checkpointCount[client];
+}
+
+void SetCheckpointCount(int client, int cpCount)
+{
+ checkpointCount[client] = cpCount;
+}
+
+int GetTeleportCount(int client)
+{
+ return teleportCount[client];
+}
+
+void SetTeleportCount(int client, int tpCount)
+{
+ teleportCount[client] = tpCount;
+}
+
+// CHECKPOINT
+
+void OnMapStart_Checkpoints()
+{
+ for (int client = 0; client < MAXPLAYERS + 1; client++)
+ {
+ if (checkpoints[client] != INVALID_HANDLE)
+ {
+ delete checkpoints[client];
+ }
+ checkpoints[client] = new ArrayList(sizeof(Checkpoint));
+ }
+}
+
+void MakeCheckpoint(int client)
+{
+ if (!CanMakeCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnMakeCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ // Make Checkpoint
+ checkpointCount[client]++;
+ Checkpoint cp;
+ cp.Create(client);
+
+ if (checkpoints[client] == INVALID_HANDLE)
+ {
+ checkpoints[client] = new ArrayList(sizeof(Checkpoint));
+ }
+
+ checkpointIndex[client] = NextIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS);
+ checkpointIndexEnd[client] = checkpointIndex[client];
+ // The list has yet to be filled up, do PushArray instead of SetArray
+ if (checkpoints[client].Length < GOKZ_MAX_CHECKPOINTS && checkpointIndex[client] == checkpoints[client].Length)
+ {
+ checkpoints[client].PushArray(cp);
+ // Initialize start and end index for the first checkpoint
+ if (checkpoints[client].Length == 1)
+ {
+ checkpointIndexStart[client] = 0;
+ checkpointIndexEnd[client] = 0;
+ }
+ }
+ else
+ {
+ checkpoints[client].SetArray(checkpointIndex[client], cp);
+ // The new checkpoint has overridden the oldest checkpoint, move the start index by one.
+ if (checkpointIndexEnd[client] == checkpointIndexStart[client])
+ {
+ checkpointIndexStart[client] = NextIndex(checkpointIndexStart[client], GOKZ_MAX_CHECKPOINTS);
+ }
+ }
+
+
+ if (GOKZ_GetCoreOption(client, Option_CheckpointSounds) == CheckpointSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_CHECKPOINT, _, "Checkpoint");
+ }
+ if (GOKZ_GetCoreOption(client, Option_CheckpointMessages) == CheckpointMessages_Enabled)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Make Checkpoint", checkpointCount[client]);
+ }
+
+ if (!GetTimerRunning(client) && AntiCpTriggerIsTouched(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Anti Checkpoint Area Warning");
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnMakeCheckpoint_Post(client);
+}
+
+bool CanMakeCheckpoint(int client, bool showError = false)
+{
+ if (!IsPlayerAlive(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Must Be Alive");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (GetTimerRunning(client) && AntiCpTriggerIsTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Anti Checkpoint Area)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (!Movement_GetOnGround(client) && Movement_GetMovetype(client) != MOVETYPE_LADDER)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (BhopTriggersJustTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+ArrayList GetCheckpointData(int client)
+{
+ // Don't clone the entire thing, return an ordered list of checkpoints.
+ // Doing this should be cleaner, saves memory and should be faster than a full Clone().
+ ArrayList checkpointData = new ArrayList(sizeof(Checkpoint));
+ if (checkpointIndex[client] == -1)
+ {
+ // No checkpoint was made, return empty ArrayList
+ return checkpointData;
+ }
+ for (int i = checkpointIndexStart[client]; i != checkpointIndexEnd[client]; i = NextIndex(i, GOKZ_MAX_CHECKPOINTS))
+ {
+ Checkpoint cp;
+ checkpoints[client].GetArray(i, cp);
+ checkpointData.PushArray(cp);
+ }
+ return checkpointData;
+}
+
+bool SetCheckpointData(int client, ArrayList cps, int version)
+{
+ if (version != GOKZ_CHECKPOINT_VERSION)
+ {
+ return false;
+ }
+ // cps is assumed to be ordered.
+ if (cps != INVALID_HANDLE)
+ {
+ delete checkpoints[client];
+ checkpoints[client] = cps.Clone();
+ if (cps.Length == 0)
+ {
+ checkpointIndexStart[client] = -1;
+ checkpointIndexEnd[client] = -1;
+ }
+ else
+ {
+ checkpointIndexStart[client] = 0;
+ checkpointIndexEnd[client] = checkpoints[client].Length - 1;
+ }
+ checkpointIndex[client] = checkpointIndexEnd[client];
+ return true;
+ }
+ return false;
+}
+
+ArrayList GetUndoTeleportData(int client)
+{
+ // Enum structs cannot be sent directly over natives, we put it in an ArrayList of one instead.
+ // We use another struct instead of reusing Checkpoint so normal checkpoints don't use more memory than needed.
+ ArrayList undoTeleportDataArray = new ArrayList(sizeof(UndoTeleportData));
+ undoTeleportDataArray.PushArray(undoTeleportData[client]);
+ return undoTeleportDataArray;
+}
+
+bool SetUndoTeleportData(int client, ArrayList undoTeleportDataArray, int version)
+{
+ if (version != GOKZ_CHECKPOINT_VERSION)
+ {
+ return false;
+ }
+ if (undoTeleportDataArray != INVALID_HANDLE && undoTeleportDataArray.Length == 1)
+ {
+ undoTeleportDataArray.GetArray(0, undoTeleportData[client], sizeof(UndoTeleportData));
+ return true;
+ }
+ return false;
+}
+// TELEPORT
+
+void TeleportToCheckpoint(int client)
+{
+ if (!CanTeleportToCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ CheckpointTeleportDo(client);
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToCheckpoint_Post(client);
+}
+
+bool CanTeleportToCheckpoint(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client) && GOKZ_GetTeleportCount(client) == 0)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (checkpoints[client] == INVALID_HANDLE || checkpoints[client].Length <= 0)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (No Checkpoints)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+// PREV CP
+
+void PrevCheckpoint(int client)
+{
+ if (!CanPrevCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnPrevCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ checkpointIndex[client] = PrevIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS);
+ CheckpointTeleportDo(client);
+
+ // Call Post Forward
+ Call_GOKZ_OnPrevCheckpoint_Post(client);
+}
+
+bool CanPrevCheckpoint(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (checkpointIndex[client] == checkpointIndexStart[client])
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Prev CP (No Checkpoints)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+// NEXT CP
+
+void NextCheckpoint(int client)
+{
+ if (!CanNextCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnNextCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+ checkpointIndex[client] = NextIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS);
+ CheckpointTeleportDo(client);
+
+ // Call Post Forward
+ Call_GOKZ_OnNextCheckpoint_Post(client);
+}
+
+bool CanNextCheckpoint(int client, bool showError = false)
+{
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (checkpointIndex[client] == checkpointIndexEnd[client])
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Next CP (No Checkpoints)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+// RESTART & RESPAWN
+
+bool CanTeleportToStart(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ float currentTime = GetEngineTime();
+ float timeSinceLastAttempt = currentTime - lastRestartAttemptTime[client];
+ float cooldown;
+ // If the client restarts for the first time or the last attempt is too long ago, restart the cooldown.
+ if (lastRestartAttemptTime[client] == 0.0 || timeSinceLastAttempt > GOKZ_SAFEGUARD_RESTART_MAX_DELAY)
+ {
+ lastRestartAttemptTime[client] = currentTime;
+ cooldown = GOKZ_SAFEGUARD_RESTART_MIN_DELAY;
+ }
+ else
+ {
+ cooldown = GOKZ_SAFEGUARD_RESTART_MIN_DELAY - timeSinceLastAttempt;
+ }
+ if (cooldown <= 0.0)
+ {
+ lastRestartAttemptTime[client] = 0.0;
+ return true;
+ }
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked (Temp)", cooldown);
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TeleportToStart(int client)
+{
+ if (!CanTeleportToStart(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToStart(client, GetCurrentCourse(client), result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ // Teleport to Start
+ if (startType[client] == StartPositionType_Spawn)
+ {
+ GOKZ_RespawnPlayer(client, .restorePos = false);
+ // Respawning alone does not guarantee a valid spawn.
+ float spawnOrigin[3];
+ float spawnAngles[3];
+ GetValidSpawn(spawnOrigin, spawnAngles);
+ TeleportPlayer(client, spawnOrigin, spawnAngles);
+ }
+ else if (startType[client] == StartPositionType_Custom)
+ {
+ TeleportDo(client, customStartOrigin[client], customStartAngles[client]);
+ }
+ else
+ {
+ TeleportDo(client, nonCustomStartOrigin[client], nonCustomStartAngles[client]);
+ }
+
+ if (startType[client] != StartPositionType_MapButton
+ && (!InRangeOfVirtualStart(client) || !CanReachVirtualStart(client)))
+ {
+ GOKZ_StopTimer(client, false);
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToStart_Post(client, GetCurrentCourse(client));
+}
+
+void TeleportToSearchStart(int client, int course)
+{
+ if (!CanTeleportToStart(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ float origin[3], angles[3];
+ if (!GetSearchStartPosition(course, origin, angles))
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found (Bonus)", course);
+ }
+ return;
+ }
+ GOKZ_StopTimer(client, false);
+
+ TeleportDo(client, origin, angles);
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToStart_Post(client, course);
+}
+
+StartPositionType GetStartPosition(int client, float position[3], float angles[3])
+{
+ if (startType[client] == StartPositionType_Custom)
+ {
+ position = customStartOrigin[client];
+ angles = customStartAngles[client];
+ }
+ else if (startType[client] != StartPositionType_Spawn)
+ {
+ position = nonCustomStartOrigin[client];
+ angles = nonCustomStartAngles[client];
+ }
+
+ return startType[client];
+}
+
+bool TeleportToCourseStart(int client, int course)
+{
+ if (!CanTeleportToStart(client, true))
+ {
+ return false;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+ float origin[3], angles[3];
+
+ if (!GetMapStartPosition(course, origin, angles))
+ {
+ if (!GetSearchStartPosition(course, origin, angles))
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found (Bonus)", course);
+ }
+ return false;
+ }
+ }
+
+ GOKZ_StopTimer(client);
+
+ TeleportDo(client, origin, angles);
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToStart_Post(client, course);
+ return true;
+}
+
+StartPositionType GetStartPositionType(int client)
+{
+ return startType[client];
+}
+
+// Note: Use ClearStartPosition to switch off StartPositionType_Custom
+void SetStartPosition(int client, StartPositionType type, const float origin[3] = NULL_VECTOR, const float angles[3] = NULL_VECTOR)
+{
+ if (type == StartPositionType_Custom)
+ {
+ startType[client] = StartPositionType_Custom;
+
+ if (!IsNullVector(origin))
+ {
+ customStartOrigin[client] = origin;
+ }
+
+ if (!IsNullVector(angles))
+ {
+ customStartAngles[client] = angles;
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnStartPositionSet_Post(client, startType[client], customStartOrigin[client], customStartAngles[client]);
+ }
+ else
+ {
+ nonCustomStartType[client] = type;
+
+ if (!IsNullVector(origin))
+ {
+ nonCustomStartOrigin[client] = origin;
+ }
+
+ if (!IsNullVector(angles))
+ {
+ nonCustomStartAngles[client] = angles;
+ }
+
+ if (startType[client] != StartPositionType_Custom)
+ {
+ startType[client] = type;
+
+ // Call Post Forward
+ Call_GOKZ_OnStartPositionSet_Post(client, startType[client], nonCustomStartOrigin[client], nonCustomStartAngles[client]);
+ }
+ }
+}
+
+void SetStartPositionToCurrent(int client, StartPositionType type)
+{
+ float origin[3], angles[3];
+ Movement_GetOrigin(client, origin);
+ Movement_GetEyeAngles(client, angles);
+
+ SetStartPosition(client, type, origin, angles);
+}
+
+bool SetStartPositionToMapStart(int client, int course)
+{
+ float origin[3], angles[3];
+
+ if (!GetMapStartPosition(course, origin, angles))
+ {
+ return false;
+ }
+
+ SetStartPosition(client, StartPositionType_MapStart, origin, angles);
+
+ return true;
+}
+
+bool ClearCustomStartPosition(int client)
+{
+ if (GetStartPositionType(client) != StartPositionType_Custom)
+ {
+ return false;
+ }
+
+ startType[client] = nonCustomStartType[client];
+
+ // Call Post Forward
+ Call_GOKZ_OnStartPositionSet_Post(client, startType[client], nonCustomStartOrigin[client], nonCustomStartAngles[client]);
+
+ return true;
+}
+
+
+// TELEPORT TO END
+
+bool CanTeleportToEnd(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TeleportToEnd(int client, int course)
+{
+ if (!CanTeleportToEnd(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToEnd(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ GOKZ_StopTimer(client, false);
+
+ if (!GetMapEndPosition(course, endOrigin[client], endAngles[client]))
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No End Found");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No End Found (Bonus)", course);
+ }
+ return;
+ }
+ TeleportDo(client, endOrigin[client], endAngles[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToEnd_Post(client, course);
+}
+
+void SetEndPosition(int client, const float origin[3] = NULL_VECTOR, const float angles[3] = NULL_VECTOR)
+{
+ if (!IsNullVector(origin))
+ {
+ endOrigin[client] = origin;
+ }
+ if (!IsNullVector(angles))
+ {
+ endAngles[client] = angles;
+ }
+}
+
+bool SetEndPositionToMapEnd(int client, int course)
+{
+ float origin[3], angles[3];
+
+ if (!GetMapEndPosition(course, origin, angles))
+ {
+ return false;
+ }
+
+ SetEndPosition(client, origin, angles);
+
+ return true;
+}
+
+
+// UNDO TP
+
+void UndoTeleport(int client)
+{
+ if (!CanUndoTeleport(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnUndoTeleport(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ // Undo Teleport
+ TeleportDo(client, undoTeleportData[client].origin, undoTeleportData[client].angles);
+
+ // Call Post Forward
+ Call_GOKZ_OnUndoTeleport_Post(client);
+}
+
+bool CanUndoTeleport(int client, bool showError = false)
+{
+ if (teleportCount[client] <= 0)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (No Teleports)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (!undoTeleportData[client].lastTeleportOnGround)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (TP Was Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (undoTeleportData[client].lastTeleportInBhopTrigger)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (undoTeleportData[client].lastTeleportInAntiCpTrigger)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (AntiCp)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Teleports(int client)
+{
+ checkpointCount[client] = 0;
+ checkpointIndex[client] = -1;
+ checkpointIndexStart[client] = -1;
+ checkpointIndexEnd[client] = -1;
+ teleportCount[client] = 0;
+ startType[client] = StartPositionType_Spawn;
+ nonCustomStartType[client] = StartPositionType_Spawn;
+ lastRestartAttemptTime[client] = 0.0;
+ if (checkpoints[client] != INVALID_HANDLE)
+ {
+ checkpoints[client].Clear();
+ }
+ // Set start and end position to main course if we know of it
+ SetStartPositionToMapStart(client, 0);
+ SetEndPositionToMapEnd(client, 0);
+
+}
+
+void OnTimerStart_Teleports(int client)
+{
+ checkpointCount[client] = 0;
+ checkpointIndex[client] = -1;
+ checkpointIndexStart[client] = -1;
+ checkpointIndexEnd[client] = -1;
+ teleportCount[client] = 0;
+ checkpoints[client].Clear();
+}
+
+void OnStartButtonPress_Teleports(int client, int course)
+{
+ SetStartPositionToCurrent(client, StartPositionType_MapButton);
+ SetEndPositionToMapEnd(client, course);
+}
+
+void OnVirtualStartButtonPress_Teleports(int client)
+{
+ SetStartPositionToCurrent(client, StartPositionType_MapButton);
+}
+
+void OnStartZoneStartTouch_Teleports(int client, int course)
+{
+ SetStartPositionToMapStart(client, course);
+ SetEndPositionToMapEnd(client, course);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static int PrevIndex(int current, int maximum)
+{
+ int prev = current - 1;
+ if (prev < 0)
+ {
+ return maximum - 1;
+ }
+ return prev;
+}
+
+static void TeleportDo(int client, const float destOrigin[3], const float destAngles[3])
+{
+ if (!IsPlayerAlive(client))
+ {
+ GOKZ_RespawnPlayer(client);
+ }
+
+ // Store information about where player is teleporting from
+ undoTeleportData[client].Init(client, BhopTriggersJustTouched(client), Movement_GetOnGround(client), AntiCpTriggerIsTouched(client));
+
+ teleportCount[client]++;
+ TeleportPlayer(client, destOrigin, destAngles);
+ // TeleportPlayer needs to be done before undo TP data can be fully updated.
+ undoTeleportData[client].Update();
+ if (GOKZ_GetCoreOption(client, Option_TeleportSounds) == TeleportSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TELEPORT, _, "Teleport");
+ }
+
+ // Call Post Foward
+ Call_GOKZ_OnCountedTeleport_Post(client);
+}
+
+static void CheckpointTeleportDo(int client)
+{
+ Checkpoint cp;
+ checkpoints[client].GetArray(checkpointIndex[client], cp);
+
+ TeleportDo(client, cp.origin, cp.angles);
+ if (cp.groundEnt != INVALID_ENT_REFERENCE)
+ {
+ SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", cp.groundEnt);
+ SetEntityFlags(client, GetEntityFlags(client) | FL_ONGROUND);
+ }
+ // Handle ladder stuff
+ if (cp.onLadder)
+ {
+ SetEntPropVector(client, Prop_Send, "m_vecLadderNormal", cp.ladderNormal);
+ if (!GOKZ_GetPaused(client))
+ {
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+ else
+ {
+ SetPausedOnLadder(client, true);
+ }
+ }
+ else if (GOKZ_GetPaused(client))
+ {
+ SetPausedOnLadder(client, false);
+ }
+}
diff --git a/sourcemod/scripting/gokz-core/timer/pause.sp b/sourcemod/scripting/gokz-core/timer/pause.sp
new file mode 100644
index 0000000..92ab1fb
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/pause.sp
@@ -0,0 +1,257 @@
+static bool paused[MAXPLAYERS + 1];
+static bool pausedOnLadder[MAXPLAYERS + 1];
+static float lastPauseTime[MAXPLAYERS + 1];
+static bool hasPausedInThisRun[MAXPLAYERS + 1];
+static float lastResumeTime[MAXPLAYERS + 1];
+static bool hasResumedInThisRun[MAXPLAYERS + 1];
+static float lastDuckValue[MAXPLAYERS + 1];
+static float lastStaminaValue[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetPaused(int client)
+{
+ return paused[client];
+}
+
+void SetPausedOnLadder(int client, bool onLadder)
+{
+ pausedOnLadder[client] = onLadder;
+}
+
+void Pause(int client)
+{
+ if (!CanPause(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnPause(client, result);
+ if (result != Plugin_Continue)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Generic)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ // Pause
+ paused[client] = true;
+ pausedOnLadder[client] = Movement_GetMovetype(client) == MOVETYPE_LADDER;
+ lastDuckValue[client] = Movement_GetDuckSpeed(client);
+ lastStaminaValue[client] = GetEntPropFloat(client, Prop_Send, "m_flStamina");
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ Movement_SetMovetype(client, MOVETYPE_NONE);
+ if (GetTimerRunning(client))
+ {
+ hasPausedInThisRun[client] = true;
+ lastPauseTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnPause_Post(client);
+}
+
+bool CanPause(int client, bool showError = false)
+{
+ if (paused[client])
+ {
+ return false;
+ }
+
+ if (GetTimerRunning(client))
+ {
+ if (hasResumedInThisRun[client]
+ && GetEngineTime() - lastResumeTime[client] < GOKZ_PAUSE_COOLDOWN)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Resumed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (!Movement_GetOnGround(client)
+ && !(Movement_GetSpeed(client) == 0 && Movement_GetVerticalVelocity(client) == 0))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (BhopTriggersJustTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (AntiPauseTriggerIsTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Anti Pause Area)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Resume(int client, bool force = false)
+{
+ if (!paused[client])
+ {
+ return;
+ }
+ if (!force && !CanResume(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnResume(client, result);
+ if (result != Plugin_Continue)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Generic)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ // Resume
+ if (pausedOnLadder[client])
+ {
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+ else
+ {
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+ Movement_SetDuckSpeed(client, lastDuckValue[client]);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+bool CanResume(int client, bool showError = false)
+{
+ if (GetTimerRunning(client) && hasPausedInThisRun[client]
+ && GetEngineTime() - lastPauseTime[client] < GOKZ_PAUSE_COOLDOWN)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Just Paused)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TogglePause(int client)
+{
+ if (paused[client])
+ {
+ Resume(client);
+ }
+ else
+ {
+ Pause(client);
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Pause(int client)
+{
+ paused[client] = false;
+}
+
+void OnTimerStart_Pause(int client)
+{
+ hasPausedInThisRun[client] = false;
+ hasResumedInThisRun[client] = false;
+ Resume(client, true);
+}
+
+void OnChangeMovetype_Pause(int client, MoveType newMovetype)
+{
+ // Check if player has escaped MOVETYPE_NONE
+ if (!paused[client] || newMovetype == MOVETYPE_NONE)
+ {
+ return;
+ }
+
+ // Player has escaped MOVETYPE_NONE, so resume
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+void OnPlayerSpawn_Pause(int client)
+{
+ if (!paused[client])
+ {
+ return;
+ }
+
+ // Player has left paused state by spawning in, so resume
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+
+ Movement_SetDuckSpeed(client, lastDuckValue[client]);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+void OnJoinTeam_Pause(int client, int team)
+{
+ // Only handle joining spectators. Joining other teams is handled by OnPlayerSpawn.
+ if (team == CS_TEAM_SPECTATOR)
+ {
+ paused[client] = true;
+
+ if (GetTimerRunning(client))
+ {
+ hasPausedInThisRun[client] = true;
+ lastPauseTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnPause_Post(client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/timer/timer.sp b/sourcemod/scripting/gokz-core/timer/timer.sp
new file mode 100644
index 0000000..f6696ac
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/timer.sp
@@ -0,0 +1,368 @@
+static bool timerRunning[MAXPLAYERS + 1];
+static float currentTime[MAXPLAYERS + 1];
+static int currentCourse[MAXPLAYERS + 1];
+static float lastEndTime[MAXPLAYERS + 1];
+static float lastFalseEndTime[MAXPLAYERS + 1];
+static float lastStartSoundTime[MAXPLAYERS + 1];
+static int lastStartMode[MAXPLAYERS + 1];
+static bool validTime[MAXPLAYERS + 1];
+
+
+// =====[ PUBLIC ]=====
+
+bool GetTimerRunning(int client)
+{
+ return timerRunning[client];
+}
+
+bool GetValidTimer(int client)
+{
+ return validTime[client];
+}
+
+float GetCurrentTime(int client)
+{
+ return currentTime[client];
+}
+
+void SetCurrentTime(int client, float time)
+{
+ currentTime[client] = time;
+ // The timer should be running if time is not negative.
+ timerRunning[client] = time >= 0.0;
+}
+
+int GetCurrentCourse(int client)
+{
+ return currentCourse[client];
+}
+
+void SetCurrentCourse(int client, int course)
+{
+ currentCourse[client] = course;
+}
+
+int GetCurrentTimeType(int client)
+{
+ if (GetTeleportCount(client) == 0)
+ {
+ return TimeType_Pro;
+ }
+ return TimeType_Nub;
+}
+
+bool TimerStart(int client, int course, bool allowMidair = false, bool playSound = true)
+{
+ if (!IsPlayerAlive(client)
+ || JustStartedTimer(client)
+ || JustTeleported(client)
+ || JustNoclipped(client)
+ || !IsPlayerValidMoveType(client)
+ || !allowMidair && (!Movement_GetOnGround(client) || JustLanded(client))
+ || allowMidair && !Movement_GetOnGround(client) && (!GOKZ_GetValidJump(client) || GOKZ_GetHitPerf(client))
+ || (GOKZ_GetTimerRunning(client) && GOKZ_GetCourse(client) != course))
+ {
+ return false;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTimerStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Start Timer
+ currentTime[client] = 0.0;
+ timerRunning[client] = true;
+ currentCourse[client] = course;
+ lastStartMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ validTime[client] = true;
+ if (playSound)
+ {
+ PlayTimerStartSound(client);
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTimerStart_Post(client, course);
+
+ return true;
+}
+
+bool TimerEnd(int client, int course)
+{
+ if (!IsPlayerAlive(client))
+ {
+ return false;
+ }
+
+ if (!timerRunning[client] || course != currentCourse[client])
+ {
+ PlayTimerFalseEndSound(client);
+ lastFalseEndTime[client] = GetGameTime();
+ return false;
+ }
+
+ float time = GetCurrentTime(client);
+ int teleportsUsed = GetTeleportCount(client);
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTimerEnd(client, course, time, teleportsUsed, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+
+ if (!validTime[client])
+ {
+ PlayTimerFalseEndSound(client);
+ lastFalseEndTime[client] = GetGameTime();
+ TimerStop(client, false);
+ return false;
+ }
+ // End Timer
+ timerRunning[client] = false;
+ lastEndTime[client] = GetGameTime();
+ PlayTimerEndSound(client);
+
+ if (!IsFakeClient(client))
+ {
+ // Print end timer message
+ Call_GOKZ_OnTimerEndMessage(client, course, time, teleportsUsed, result);
+ if (result == Plugin_Continue)
+ {
+ PrintEndTimeString(client);
+ }
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTimerEnd_Post(client, course, time, teleportsUsed);
+
+ return true;
+}
+
+bool TimerStop(int client, bool playSound = true)
+{
+ if (!timerRunning[client])
+ {
+ return false;
+ }
+
+ timerRunning[client] = false;
+ if (playSound)
+ {
+ PlayTimerStopSound(client);
+ }
+
+ Call_GOKZ_OnTimerStopped(client);
+
+ return true;
+}
+
+void TimerStopAll(bool playSound = true)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client))
+ {
+ TimerStop(client, playSound);
+ }
+ }
+}
+
+void PlayTimerStartSound(int client)
+{
+ if (GetGameTime() - lastStartSoundTime[client] > GOKZ_TIMER_SOUND_COOLDOWN)
+ {
+ GOKZ_EmitSoundToClient(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ lastStartSoundTime[client] = GetGameTime();
+ }
+}
+
+void InvalidateRun(int client)
+{
+ if (validTime[client])
+ {
+ validTime[client] = false;
+ Call_GOKZ_OnRunInvalidated(client);
+ }
+}
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Timer(int client)
+{
+ timerRunning[client] = false;
+ currentTime[client] = 0.0;
+ currentCourse[client] = 0;
+ lastEndTime[client] = 0.0;
+ lastFalseEndTime[client] = 0.0;
+ lastStartSoundTime[client] = 0.0;
+ lastStartMode[client] = MODE_COUNT; // So it won't equal any mode
+}
+
+void OnPlayerRunCmdPost_Timer(int client)
+{
+ if (IsPlayerAlive(client) && GetTimerRunning(client) && !GetPaused(client))
+ {
+ currentTime[client] += GetTickInterval();
+ }
+}
+
+void OnChangeMovetype_Timer(int client, MoveType newMovetype)
+{
+ if (!IsValidMovetype(newMovetype))
+ {
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Noclipped)");
+ }
+ }
+}
+
+void OnTeleportToStart_Timer(int client)
+{
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro)
+ {
+ TimerStop(client, false);
+ }
+}
+
+void OnClientDisconnect_Timer(int client)
+{
+ TimerStop(client);
+}
+
+void OnPlayerDeath_Timer(int client)
+{
+ TimerStop(client);
+}
+
+void OnOptionChanged_Timer(int client, Option option)
+{
+ if (option == Option_Mode)
+ {
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Changed Mode)");
+ }
+ }
+}
+
+void OnRoundStart_Timer()
+{
+ TimerStopAll();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static bool IsPlayerValidMoveType(int client)
+{
+ return IsValidMovetype(Movement_GetMovetype(client));
+}
+
+static bool IsValidMovetype(MoveType movetype)
+{
+ return movetype == MOVETYPE_WALK
+ || movetype == MOVETYPE_LADDER
+ || movetype == MOVETYPE_NONE
+ || movetype == MOVETYPE_OBSERVER;
+}
+
+static bool JustTeleported(int client)
+{
+ return gB_OriginTeleported[client] || gB_VelocityTeleported[client]
+ || gI_CmdNum[client] - gI_TeleportCmdNum[client] <= GOKZ_TIMER_START_GROUND_TICKS;
+}
+
+static bool JustLanded(int client)
+{
+ return !gB_OldOnGround[client]
+ || gI_CmdNum[client] - Movement_GetLandingCmdNum(client) <= GOKZ_TIMER_START_NO_TELEPORT_TICKS;
+}
+
+static bool JustStartedTimer(int client)
+{
+ return timerRunning[client] && GetCurrentTime(client) < EPSILON;
+}
+
+static bool JustEndedTimer(int client)
+{
+ return GetGameTime() - lastEndTime[client] < 1.0;
+}
+
+static void PlayTimerEndSound(int client)
+{
+ GOKZ_EmitSoundToClient(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+}
+
+static void PlayTimerFalseEndSound(int client)
+{
+ if (!JustEndedTimer(client)
+ && (GetGameTime() - lastFalseEndTime[client]) > GOKZ_TIMER_SOUND_COOLDOWN)
+ {
+ GOKZ_EmitSoundToClient(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End");
+ }
+}
+
+static void PlayTimerStopSound(int client)
+{
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+ GOKZ_EmitSoundToClientSpectators(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+}
+
+static void PrintEndTimeString(int client)
+{
+ if (GetCurrentCourse(client) == 0)
+ {
+ switch (GetCurrentTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Map (NUB)",
+ client,
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Map (PRO)",
+ client,
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+ else
+ {
+ switch (GetCurrentTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (NUB)",
+ client,
+ currentCourse[client],
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (PRO)",
+ client,
+ currentCourse[client],
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp
new file mode 100644
index 0000000..aa88a9d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp
@@ -0,0 +1,322 @@
+/*
+ Most commonly referred to in the KZ community as timer tech.
+ Lets players press 'virtual' start and end buttons without looking.
+*/
+
+
+
+static int beamSprite;
+static int haloSprite;
+static float lastUsePressTime[MAXPLAYERS + 1];
+static int lastTeleportTick[MAXPLAYERS + 1];
+static bool startedTimerLastTick[MAXPLAYERS + 1];
+static bool onlyNaturalButtonPressed[MAXPLAYERS + 1];
+static int startTimerButtonPressTick[MAXPLAYERS + 1];
+static bool hasEndedTimerSincePressingUse[MAXPLAYERS + 1];
+static bool hasTeleportedSincePressingUse[MAXPLAYERS + 1];
+static bool hasVirtualStartButton[MAXPLAYERS + 1];
+static bool hasVirtualEndButton[MAXPLAYERS + 1];
+static bool wasInEndZone[MAXPLAYERS + 1];
+static float virtualStartOrigin[MAXPLAYERS + 1][3];
+static float virtualEndOrigin[MAXPLAYERS + 1][3];
+static int virtualStartCourse[MAXPLAYERS + 1];
+static int virtualEndCourse[MAXPLAYERS + 1];
+static bool virtualButtonsLocked[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetHasVirtualStartButton(int client)
+{
+ return hasVirtualStartButton[client];
+}
+
+bool GetHasVirtualEndButton(int client)
+{
+ return hasVirtualEndButton[client];
+}
+
+bool ToggleVirtualButtonsLock(int client)
+{
+ virtualButtonsLocked[client] = !virtualButtonsLocked[client];
+ return virtualButtonsLocked[client];
+}
+
+void LockVirtualButtons(int client)
+{
+ virtualButtonsLocked[client] = true;
+}
+
+int GetVirtualButtonPosition(int client, float position[3], bool isStart)
+{
+ if (isStart && hasVirtualStartButton[client])
+ {
+ position = virtualStartOrigin[client];
+ return virtualStartCourse[client];
+ }
+ else if (!isStart && hasVirtualEndButton[client])
+ {
+ position = virtualEndOrigin[client];
+ return virtualEndCourse[client];
+ }
+
+ return -1;
+}
+
+void SetVirtualButtonPosition(int client, float position[3], int course, bool isStart)
+{
+ if (isStart)
+ {
+ virtualStartCourse[client] = course;
+ virtualStartOrigin[client] = position;
+ hasVirtualStartButton[client] = true;
+ }
+ else
+ {
+ virtualEndCourse[client] = course;
+ virtualEndOrigin[client] = position;
+ hasVirtualEndButton[client] = true;
+ }
+}
+
+void ResetVirtualButtonPosition(int client, bool isStart)
+{
+ if (isStart)
+ {
+ virtualStartCourse[client] = -1;
+ virtualStartOrigin[client] = {0.0, 0.0, 0.0};
+ hasVirtualStartButton[client] = false;
+ }
+ else
+ {
+ virtualEndCourse[client] = -1;
+ virtualEndOrigin[client] = {0.0, 0.0, 0.0};
+ hasVirtualEndButton[client] = false;
+ }
+}
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_VirtualButtons()
+{
+ beamSprite = PrecacheModel("materials/sprites/laserbeam.vmt");
+ haloSprite = PrecacheModel("materials/sprites/glow01.vmt");
+}
+
+void OnClientPutInServer_VirtualButtons(int client)
+{
+ startedTimerLastTick[client] = false;
+ hasVirtualEndButton[client] = false;
+ hasVirtualStartButton[client] = false;
+ virtualButtonsLocked[client] = false;
+ onlyNaturalButtonPressed[client] = false;
+ wasInEndZone[client] = false;
+ startTimerButtonPressTick[client] = 0;
+}
+
+void OnStartButtonPress_VirtualButtons(int client, int course)
+{
+ if (!virtualButtonsLocked[client] &&
+ lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount())
+ {
+ Movement_GetOrigin(client, virtualStartOrigin[client]);
+ virtualStartCourse[client] = course;
+ hasVirtualStartButton[client] = true;
+ startTimerButtonPressTick[client] = GetGameTickCount();
+ }
+}
+
+void OnEndButtonPress_VirtualButtons(int client, int course)
+{
+ // Prevent setting end virtual button to where it would usually be unreachable
+ if (IsPlayerStuck(client))
+ {
+ return;
+ }
+
+ if (!virtualButtonsLocked[client] &&
+ lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount())
+ {
+ Movement_GetOrigin(client, virtualEndOrigin[client]);
+ virtualEndCourse[client] = course;
+ hasVirtualEndButton[client] = true;
+ }
+}
+
+void OnPlayerRunCmdPost_VirtualButtons(int client, int buttons, int cmdnum)
+{
+ CheckForAndHandleUsage(client, buttons);
+ UpdateIndicators(client, cmdnum);
+}
+
+void OnCountedTeleport_VirtualButtons(int client)
+{
+ hasTeleportedSincePressingUse[client] = true;
+}
+
+void OnTeleport_DelayVirtualButtons(int client)
+{
+ lastTeleportTick[client] = GetGameTickCount();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CheckForAndHandleUsage(int client, int buttons)
+{
+ if (buttons & IN_USE && !(gI_OldButtons[client] & IN_USE))
+ {
+ lastUsePressTime[client] = GetGameTime();
+ hasEndedTimerSincePressingUse[client] = false;
+ hasTeleportedSincePressingUse[client] = false;
+ onlyNaturalButtonPressed[client] = startTimerButtonPressTick[client] == GetGameTickCount();
+ }
+
+ bool useCheck = PassesUseCheck(client);
+
+ // Start button
+ if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) == TimerButtonZoneType_BothZones)
+ && GetHasVirtualStartButton(client) && InRangeOfVirtualStart(client) && CanReachVirtualStart(client))
+ {
+ if (TimerStart(client, virtualStartCourse[client], .playSound = false))
+ {
+ startedTimerLastTick[client] = true;
+ OnVirtualStartButtonPress_Teleports(client);
+ }
+ }
+ else if (startedTimerLastTick[client])
+ {
+ // Without that check you get two sounds when pressing the natural timer button
+ if (!onlyNaturalButtonPressed[client])
+ {
+ PlayTimerStartSound(client);
+ }
+ onlyNaturalButtonPressed[client] = false;
+ startedTimerLastTick[client] = false;
+ }
+
+ // End button
+ if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) != TimerButtonZoneType_BothButtons)
+ && GetHasVirtualEndButton(client) && InRangeOfVirtualEnd(client) && CanReachVirtualEnd(client))
+ {
+ if (!wasInEndZone[client])
+ {
+ TimerEnd(client, virtualEndCourse[client]);
+ hasEndedTimerSincePressingUse[client] = true; // False end counts as well
+ wasInEndZone[client] = true;
+ }
+ }
+ else
+ {
+ wasInEndZone[client] = false;
+ }
+}
+
+static bool PassesUseCheck(int client)
+{
+ if (GetGameTime() - lastUsePressTime[client] < GOKZ_VIRTUAL_BUTTON_USE_DETECTION_TIME + EPSILON
+ && !hasEndedTimerSincePressingUse[client]
+ && !hasTeleportedSincePressingUse[client])
+ {
+ return true;
+ }
+
+ return false;
+}
+
+bool InRangeOfVirtualStart(int client)
+{
+ return InRangeOfButton(client, virtualStartOrigin[client]);
+}
+
+static bool InRangeOfVirtualEnd(int client)
+{
+ return InRangeOfButton(client, virtualEndOrigin[client]);
+}
+
+static bool InRangeOfButton(int client, const float buttonOrigin[3])
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ float distanceToButton = GetVectorDistance(origin, buttonOrigin);
+ return distanceToButton <= gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)];
+}
+
+bool CanReachVirtualStart(int client)
+{
+ return CanReachButton(client, virtualStartOrigin[client]);
+}
+
+static bool CanReachVirtualEnd(int client)
+{
+ return CanReachButton(client, virtualEndOrigin[client]);
+}
+
+static bool CanReachButton(int client, const float buttonOrigin[3])
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ Handle trace = TR_TraceRayFilterEx(origin, buttonOrigin, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers);
+ bool didHit = TR_DidHit(trace);
+ delete trace;
+ return !didHit;
+}
+
+
+
+// ===== [ INDICATOR ] =====
+
+static void UpdateIndicators(int client, int cmdnum)
+{
+ if (cmdnum % 128 != 0 || !IsPlayerAlive(client)
+ || GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled)
+ {
+ return;
+ }
+
+ if (hasVirtualStartButton[client])
+ {
+ DrawIndicator(client, virtualStartOrigin[client], { 0, 255, 0, 255 } );
+ }
+
+ if (hasVirtualEndButton[client])
+ {
+ DrawIndicator(client, virtualEndOrigin[client], { 255, 0, 0, 255 } );
+ }
+}
+
+static void DrawIndicator(int client, const float origin[3], const int colour[4])
+{
+ float radius = gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)];
+ if (radius <= EPSILON) // Don't draw circle of radius 0
+ {
+ return;
+ }
+
+ float x, y, start[3], end[3];
+
+ // Create the start position for the first part of the beam
+ start[0] = origin[0] + radius;
+ start[1] = origin[1];
+ start[2] = origin[2];
+
+ for (int i = 1; i <= 31; i++) // Circle is broken into 31 segments
+ {
+ float angle = 2 * PI / 31 * i;
+ x = radius * Cosine(angle);
+ y = radius * Sine(angle);
+
+ end[0] = origin[0] + x;
+ end[1] = origin[1] + y;
+ end[2] = origin[2];
+
+ TE_SetupBeamPoints(start, end, beamSprite, haloSprite, 0, 0, 0.97, 0.2, 0.2, 0, 0.0, colour, 0);
+ TE_SendToClient(client);
+
+ start[0] = end[0];
+ start[1] = end[1];
+ start[2] = end[2];
+ }
+}
diff --git a/sourcemod/scripting/gokz-core/triggerfix.sp b/sourcemod/scripting/gokz-core/triggerfix.sp
new file mode 100644
index 0000000..424928d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/triggerfix.sp
@@ -0,0 +1,622 @@
+
+
+// Credits:
+// RNGFix made by rio https://github.com/jason-e/rngfix
+
+
+// Engine constants, NOT settings (do not change)
+#define LAND_HEIGHT 2.0 // Maximum height above ground at which you can "land"
+#define MIN_STANDABLE_ZNRM 0.7 // Minimum surface normal Z component of a walkable surface
+
+static int processMovementTicks[MAXPLAYERS+1];
+static float playerFrameTime[MAXPLAYERS+1];
+
+static bool touchingTrigger[MAXPLAYERS+1][2048];
+static int triggerTouchFired[MAXPLAYERS+1][2048];
+static int lastGroundEnt[MAXPLAYERS + 1];
+static bool duckedLastTick[MAXPLAYERS + 1];
+static bool mapTeleportedSequentialTicks[MAXPLAYERS+1];
+static bool jumpBugged[MAXPLAYERS + 1];
+static float jumpBugOrigin[MAXPLAYERS + 1][3];
+
+static ConVar cvGravity;
+
+static Handle acceptInputHookPre;
+static Handle processMovementHookPre;
+static Address serverGameEnts;
+static Handle markEntitiesAsTouching;
+static Handle passesTriggerFilters;
+
+public void OnPluginStart_Triggerfix()
+{
+ HookEvent("player_jump", Event_PlayerJump);
+
+ cvGravity = FindConVar("sv_gravity");
+ if (cvGravity == null)
+ {
+ SetFailState("Could not find sv_gravity");
+ }
+
+ GameData gamedataConf = LoadGameConfigFile("gokz-core.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-core gamedata");
+ }
+
+ // PassesTriggerFilters
+ StartPrepSDKCall(SDKCall_Entity);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "CBaseTrigger::PassesTriggerFilters"))
+ {
+ SetFailState("Failed to get CBaseTrigger::PassesTriggerFilters offset");
+ }
+ PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer);
+ passesTriggerFilters = EndPrepSDKCall();
+
+ if (passesTriggerFilters == null) SetFailState("Unable to prepare SDKCall for CBaseTrigger::PassesTriggerFilters");
+
+ // CreateInterface
+ // Thanks SlidyBat and ici
+ StartPrepSDKCall(SDKCall_Static);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Signature, "CreateInterface"))
+ {
+ SetFailState("Failed to get CreateInterface");
+ }
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+ Handle CreateInterface = EndPrepSDKCall();
+
+ if (CreateInterface == null)
+ {
+ SetFailState("Unable to prepare SDKCall for CreateInterface");
+ }
+
+ char interfaceName[64];
+
+ // ProcessMovement
+ if (!GameConfGetKeyValue(gamedataConf, "IGameMovement", interfaceName, sizeof(interfaceName)))
+ {
+ SetFailState("Failed to get IGameMovement interface name");
+ }
+ Address IGameMovement = SDKCall(CreateInterface, interfaceName, 0);
+ if (!IGameMovement)
+ {
+ SetFailState("Failed to get IGameMovement pointer");
+ }
+
+ int offset = GameConfGetOffset(gamedataConf, "ProcessMovement");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get ProcessMovement offset");
+ }
+
+ processMovementHookPre = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPre);
+ DHookAddParam(processMovementHookPre, HookParamType_CBaseEntity);
+ DHookAddParam(processMovementHookPre, HookParamType_ObjectPtr);
+ DHookRaw(processMovementHookPre, false, IGameMovement);
+
+ // MarkEntitiesAsTouching
+ if (!GameConfGetKeyValue(gamedataConf, "IServerGameEnts", interfaceName, sizeof(interfaceName)))
+ {
+ SetFailState("Failed to get IServerGameEnts interface name");
+ }
+ serverGameEnts = SDKCall(CreateInterface, interfaceName, 0);
+ if (!serverGameEnts)
+ {
+ SetFailState("Failed to get IServerGameEnts pointer");
+ }
+
+ StartPrepSDKCall(SDKCall_Raw);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "IServerGameEnts::MarkEntitiesAsTouching"))
+ {
+ SetFailState("Failed to get IServerGameEnts::MarkEntitiesAsTouching offset");
+ }
+ PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer);
+ markEntitiesAsTouching = EndPrepSDKCall();
+
+ if (markEntitiesAsTouching == null)
+ {
+ SetFailState("Unable to prepare SDKCall for IServerGameEnts::MarkEntitiesAsTouching");
+ }
+
+ gamedataConf = LoadGameConfigFile("sdktools.games/engine.csgo");
+ offset = gamedataConf.GetOffset("AcceptInput");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get AcceptInput offset");
+ }
+
+ acceptInputHookPre = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, DHooks_AcceptInput);
+ DHookAddParam(acceptInputHookPre, HookParamType_CharPtr);
+ DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity);
+ DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity);
+ //varaint_t is a union of 12 (float[3]) plus two int type params 12 + 8 = 20
+ DHookAddParam(acceptInputHookPre, HookParamType_Object, 20, DHookPass_ByVal|DHookPass_ODTOR|DHookPass_OCTOR|DHookPass_OASSIGNOP);
+ DHookAddParam(acceptInputHookPre, HookParamType_Int);
+
+ delete CreateInterface;
+ delete gamedataConf;
+
+ if (gB_LateLoad)
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client)) OnClientPutInServer(client);
+ }
+
+ char classname[64];
+ for (int entity = MaxClients+1; entity < sizeof(touchingTrigger[]); entity++)
+ {
+ if (!IsValidEntity(entity)) continue;
+ GetEntPropString(entity, Prop_Data, "m_iClassname", classname, sizeof(classname));
+ HookTrigger(entity, classname);
+ }
+ }
+}
+
+public void OnEntityCreated_Triggerfix(int entity, const char[] classname)
+{
+ if (entity >= sizeof(touchingTrigger[]))
+ {
+ return;
+ }
+ HookTrigger(entity, classname);
+}
+
+public void OnClientConnected_Triggerfix(int client)
+{
+ processMovementTicks[client] = 0;
+ for (int i = 0; i < sizeof(touchingTrigger[]); i++)
+ {
+ touchingTrigger[client][i] = false;
+ triggerTouchFired[client][i] = 0;
+ }
+}
+
+public void OnClientPutInServer_Triggerfix(int client)
+{
+ SDKHook(client, SDKHook_PostThink, Hook_PlayerPostThink);
+ DHookEntity(acceptInputHookPre, false, client);
+}
+
+public void OnGameFrame_Triggerfix()
+{
+ // Loop through all the players and make sure that triggers that are supposed to be fired but weren't now
+ // get fired properly.
+ // This must be run OUTSIDE of usercmd, because sometimes usercmd gets delayed heavily.
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && IsPlayerAlive(client) && !CheckWater(client) &&
+ (GetEntityMoveType(client) == MOVETYPE_WALK || GetEntityMoveType(client) == MOVETYPE_LADDER))
+ {
+ DoTriggerFix(client);
+
+ // Reset the Touch tracking.
+ // We save a bit of performance by putting this inside the loop
+ // Even if triggerTouchFired is not correct, touchingTrigger still is.
+ // That should prevent DoTriggerFix from activating the wrong triggers.
+ // Plus, players respawn where they previously are as well with a timer on,
+ // so this should not be a big problem.
+ for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++)
+ {
+ triggerTouchFired[client][trigger] = 0;
+ }
+ }
+ }
+}
+
+void OnPlayerRunCmd_Triggerfix(int client)
+{
+ // Reset the Touch tracking.
+ // While this is mostly unnecessary, it can also happen that the server runs multiple ticks of player movement at once,
+ // therefore the triggers need to be checked again.
+ for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++)
+ {
+ triggerTouchFired[client][trigger] = 0;
+ }
+}
+
+static void Event_PlayerJump(Event event, const char[] name, bool dontBroadcast)
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+
+ jumpBugged[client] = !!lastGroundEnt[client];
+ if (jumpBugged[client])
+ {
+ GetClientAbsOrigin(client, jumpBugOrigin[client]);
+ // if player's origin is still in the ducking position then adjust for that.
+ if (duckedLastTick[client] && !Movement_GetDucking(client))
+ {
+ jumpBugOrigin[client][2] -= 9.0;
+ }
+ }
+}
+
+static Action Hook_TriggerStartTouch(int entity, int other)
+{
+ if (1 <= other <= MaxClients)
+ {
+ touchingTrigger[other][entity] = true;
+ }
+
+ return Plugin_Continue;
+}
+
+static Action Hook_TriggerEndTouch(int entity, int other)
+{
+ if (1 <= other <= MaxClients)
+ {
+ touchingTrigger[other][entity] = false;
+ }
+ return Plugin_Continue;
+}
+
+static Action Hook_TriggerTouch(int entity, int other)
+{
+ if (1 <= other <= MaxClients)
+ {
+ triggerTouchFired[other][entity]++;
+ }
+ return Plugin_Continue;
+}
+
+static MRESReturn DHook_ProcessMovementPre(Handle hParams)
+{
+ int client = DHookGetParam(hParams, 1);
+
+ processMovementTicks[client]++;
+ playerFrameTime[client] = GetTickInterval() * GetEntPropFloat(client, Prop_Data, "m_flLaggedMovementValue");
+ mapTeleportedSequentialTicks[client] = false;
+
+ if (IsPlayerAlive(client))
+ {
+ if (GetEntityMoveType(client) == MOVETYPE_WALK
+ && !CheckWater(client))
+ {
+ lastGroundEnt[client] = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity");
+ }
+ duckedLastTick[client] = Movement_GetDucking(client);
+ }
+
+ return MRES_Ignored;
+}
+
+static MRESReturn DHooks_AcceptInput(int client, DHookReturn hReturn, DHookParam hParams)
+{
+ if (!IsValidClient(client) || !IsPlayerAlive(client) || CheckWater(client) ||
+ (GetEntityMoveType(client) != MOVETYPE_WALK && GetEntityMoveType(client) != MOVETYPE_LADDER))
+ {
+ return MRES_Ignored;
+ }
+
+ // Get args
+ static char param[64];
+ static char command[64];
+ DHookGetParamString(hParams, 1, command, sizeof(command));
+ if (StrEqual(command, "AddOutput"))
+ {
+ DHookGetParamObjectPtrString(hParams, 4, 0, ObjectValueType_String, param, sizeof(param));
+ char kv[16];
+ SplitString(param, " ", kv, sizeof(kv));
+ // KVs are case insensitive.
+ // Any of these inputs can change the filter behavior.
+ if (StrEqual(kv[0], "targetname", false) || StrEqual(kv[0], "teamnumber", false) || StrEqual(kv[0], "classname", false) || StrEqual(command, "ResponseContext", false))
+ {
+ DoTriggerFix(client, true);
+ }
+ }
+ else if (StrEqual(command, "AddContext") || StrEqual(command, "RemoveContext") || StrEqual(command, "ClearContext"))
+ {
+ DoTriggerFix(client, true);
+ }
+ return MRES_Ignored;
+}
+
+static bool DoTriggerFix(int client, bool filterFix = false)
+{
+ // Adapted from DoTriggerjumpFix right below.
+ float landingMins[3], landingMaxs[3];
+ float origin[3];
+
+ GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+ GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins);
+ GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs);
+
+ ArrayList triggers = new ArrayList();
+ // Get a list of triggers that we are touching now.
+
+ TR_EnumerateEntitiesHull(origin, origin, landingMins, landingMaxs, true, AddTrigger, triggers);
+
+ bool didSomething = false;
+
+ for (int i = 0; i < triggers.Length; i++)
+ {
+ int trigger = triggers.Get(i);
+ if (!touchingTrigger[client][trigger])
+ {
+ // Normally this wouldn't happen, because the trigger should be colliding with the player's hull if it gets here.
+ continue;
+ }
+ char className[64];
+ GetEntityClassname(trigger, className, sizeof(className));
+ if (StrEqual(className, "trigger_push"))
+ {
+ // Completely ignore push triggers.
+ continue;
+ }
+ if (filterFix && SDKCall(passesTriggerFilters, trigger, client) && triggerTouchFired[client][trigger] < GOKZ_MAX_RETOUCH_TRIGGER_COUNT)
+ {
+ // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick.
+ SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger);
+
+ // Player properties might be changed right after this so it will need to be triggered again.
+ // Triggers changing this filter will loop onto itself infintely so we need to avoid that.
+ triggerTouchFired[client][trigger]++;
+ didSomething = true;
+ }
+ else if (!triggerTouchFired[client][trigger])
+ {
+ // If the player is still touching the trigger on this tick, and Touch was not called for whatever reason
+ // in the last tick, we make sure that it is called now.
+ SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger);
+ triggerTouchFired[client][trigger]++;
+ didSomething = true;
+ }
+ }
+
+ delete triggers;
+
+ return didSomething;
+}
+
+static bool DoTriggerjumpFix(int client, const float landingPoint[3], const float landingMins[3], const float landingMaxs[3])
+{
+ // It's possible to land above a trigger but also in another trigger_teleport, have the teleport move you to
+ // another location, and then the trigger jumping fix wouldn't fire the other trigger you technically landed above,
+ // but I can't imagine a mapper would ever actually stack triggers like that.
+
+ float origin[3];
+ GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+
+ float landingMaxsBelow[3];
+ landingMaxsBelow[0] = landingMaxs[0];
+ landingMaxsBelow[1] = landingMaxs[1];
+ landingMaxsBelow[2] = origin[2] - landingPoint[2];
+
+ ArrayList triggers = new ArrayList();
+
+ // Find triggers that are between us and the ground (using the bounding box quadrant we landed with if applicable).
+ // This will fail on triggers thinner than 0.03125 unit thick, but it's highly unlikely that a mapper would put a trigger that thin.
+ TR_EnumerateEntitiesHull(landingPoint, landingPoint, landingMins, landingMaxsBelow, true, AddTrigger, triggers);
+
+ bool didSomething = false;
+
+ for (int i = 0; i < triggers.Length; i++)
+ {
+ int trigger = triggers.Get(i);
+
+ // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick.
+ // In case that could cause side-effects, manually keep track of triggers we are actually touching
+ // and don't re-touch them.
+ if (touchingTrigger[client][trigger])
+ {
+ continue;
+ }
+
+ SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger);
+ didSomething = true;
+ }
+
+ delete triggers;
+
+ return didSomething;
+}
+
+// PostThink works a little better than a ProcessMovement post hook because we need to wait for ProcessImpacts (trigger activation)
+static void Hook_PlayerPostThink(int client)
+{
+ if (!IsPlayerAlive(client)
+ || GetEntityMoveType(client) != MOVETYPE_WALK
+ || CheckWater(client))
+ {
+ return;
+ }
+
+ bool landed = (GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") != -1
+ && lastGroundEnt[client] == -1)
+ || jumpBugged[client];
+
+ float landingMins[3], landingMaxs[3], landingPoint[3];
+
+ // Get info about the ground we landed on (if we need to do landing fixes).
+ if (landed)
+ {
+ float origin[3], nrm[3], velocity[3];
+ GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+ GetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity);
+
+ if (jumpBugged[client])
+ {
+ origin = jumpBugOrigin[client];
+ }
+
+ GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins);
+ GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs);
+
+ float originBelow[3];
+ originBelow[0] = origin[0];
+ originBelow[1] = origin[1];
+ originBelow[2] = origin[2] - LAND_HEIGHT;
+
+ TR_TraceHullFilter(origin, originBelow, landingMins, landingMaxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (!TR_DidHit())
+ {
+ // This should never happen, since we know we are on the ground.
+ landed = false;
+ }
+ else
+ {
+ TR_GetPlaneNormal(null, nrm);
+
+ if (nrm[2] < MIN_STANDABLE_ZNRM)
+ {
+ // This is rare, and how the incline fix should behave isn't entirely clear because maybe we should
+ // collide with multiple faces at once in this case, but let's just get the ground we officially
+ // landed on and use that for our ground normal.
+
+ // landingMins and landingMaxs will contain the final values used to find the ground after returning.
+ if (TracePlayerBBoxForGround(origin, originBelow, landingMins, landingMaxs))
+ {
+ TR_GetPlaneNormal(null, nrm);
+ }
+ else
+ {
+ // This should also never happen.
+ landed = false;
+ }
+ }
+
+ TR_GetEndPosition(landingPoint);
+ }
+ }
+
+ // reset it here because we don't need it again
+ jumpBugged[client] = false;
+
+ // Must use TR_DidHit because if the unduck origin is closer than 0.03125 units from the ground,
+ // the trace fraction would return 0.0.
+ if (landed && TR_DidHit())
+ {
+ DoTriggerjumpFix(client, landingPoint, landingMins, landingMaxs);
+ // Check if a trigger we just touched put us in the air (probably due to a teleport).
+ if (GetEntityFlags(client) & FL_ONGROUND == 0)
+ {
+ landed = false;
+ }
+ }
+}
+
+static bool PlayerFilter(int entity, int mask)
+{
+ return !(1 <= entity <= MaxClients);
+}
+
+static void HookTrigger(int entity, const char[] classname)
+{
+ if (StrContains(classname, "trigger_") != -1)
+ {
+ SDKHook(entity, SDKHook_StartTouchPost, Hook_TriggerStartTouch);
+ SDKHook(entity, SDKHook_EndTouchPost, Hook_TriggerEndTouch);
+ SDKHook(entity, SDKHook_TouchPost, Hook_TriggerTouch);
+ }
+}
+
+static bool CheckWater(int client)
+{
+ // The cached water level is updated multiple times per tick, including after movement happens,
+ // so we can just check the cached value here.
+ return GetEntProp(client, Prop_Data, "m_nWaterLevel") > 1;
+}
+
+public bool AddTrigger(int entity, ArrayList triggers)
+{
+ TR_ClipCurrentRayToEntity(MASK_ALL, entity);
+ if (TR_DidHit())
+ {
+ triggers.Push(entity);
+ }
+
+ return true;
+}
+
+static bool TracePlayerBBoxForGround(const float origin[3], const float originBelow[3], float mins[3], float maxs[3])
+{
+ // See CGameMovement::TracePlayerBBoxForGround()
+
+ float origMins[3], origMaxs[3];
+ origMins = mins;
+ origMaxs = maxs;
+
+ float nrm[3];
+
+ mins = origMins;
+
+ // -x -y
+ maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0];
+ maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1];
+ maxs[2] = origMaxs[2];
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ // +x +y
+ mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0];
+ mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1];
+ mins[2] = origMins[2];
+
+ maxs = origMaxs;
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ // -x +y
+ mins[0] = origMins[0];
+ mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1];
+ mins[2] = origMins[2];
+
+ maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0];
+ maxs[1] = origMaxs[1];
+ maxs[2] = origMaxs[2];
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ // +x -y
+ mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0];
+ mins[1] = origMins[1];
+ mins[2] = origMins[2];
+
+ maxs[0] = origMaxs[0];
+ maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1];
+ maxs[2] = origMaxs[2];
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}