diff options
| author | navewindre <nw@moneybot.cc> | 2023-12-04 18:06:10 +0100 |
|---|---|---|
| committer | navewindre <nw@moneybot.cc> | 2023-12-04 18:06:10 +0100 |
| commit | aef0d1c1268ab7d4bc18996c9c6b4da16a40aadc (patch) | |
| tree | 43e766b51704f4ab8b383583bdc1871eeeb9c698 /sourcemod/scripting/gokz-core | |
| parent | 38f1140c11724da05a23a10385061200b907cf6e (diff) | |
bbbbbbbbwaaaaaaaaaaa
Diffstat (limited to 'sourcemod/scripting/gokz-core')
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; +} |
