From aef0d1c1268ab7d4bc18996c9c6b4da16a40aadc Mon Sep 17 00:00:00 2001 From: navewindre Date: Mon, 4 Dec 2023 18:06:10 +0100 Subject: bbbbbbbbwaaaaaaaaaaa --- sourcemod/scripting/distbugfix.sp | 1592 ++++++++++++++++ sourcemod/scripting/distbugfix/clientprefs.sp | 51 + sourcemod/scripting/gokz-anticheat.sp | 318 ++++ sourcemod/scripting/gokz-anticheat/api.sp | 174 ++ .../scripting/gokz-anticheat/bhop_tracking.sp | 336 ++++ sourcemod/scripting/gokz-anticheat/commands.sp | 76 + sourcemod/scripting/gokz-chat.sp | 309 ++++ sourcemod/scripting/gokz-core.sp | 543 ++++++ sourcemod/scripting/gokz-core/commands.sp | 385 ++++ sourcemod/scripting/gokz-core/demofix.sp | 110 ++ sourcemod/scripting/gokz-core/forwards.sp | 401 ++++ sourcemod/scripting/gokz-core/map/buttons.sp | 138 ++ sourcemod/scripting/gokz-core/map/end.sp | 155 ++ sourcemod/scripting/gokz-core/map/mapfile.sp | 502 +++++ sourcemod/scripting/gokz-core/map/prefix.sp | 48 + sourcemod/scripting/gokz-core/map/starts.sp | 219 +++ sourcemod/scripting/gokz-core/map/triggers.sp | 855 +++++++++ sourcemod/scripting/gokz-core/map/zones.sp | 183 ++ sourcemod/scripting/gokz-core/menus/mode_menu.sp | 40 + .../scripting/gokz-core/menus/options_menu.sp | 174 ++ sourcemod/scripting/gokz-core/misc.sp | 803 ++++++++ sourcemod/scripting/gokz-core/modes.sp | 106 ++ sourcemod/scripting/gokz-core/natives.sp | 647 +++++++ sourcemod/scripting/gokz-core/options.sp | 438 +++++ sourcemod/scripting/gokz-core/teamnumfix.sp | 68 + sourcemod/scripting/gokz-core/teleports.sp | 917 ++++++++++ sourcemod/scripting/gokz-core/timer/pause.sp | 257 +++ sourcemod/scripting/gokz-core/timer/timer.sp | 368 ++++ .../scripting/gokz-core/timer/virtual_buttons.sp | 322 ++++ sourcemod/scripting/gokz-core/triggerfix.sp | 622 +++++++ sourcemod/scripting/gokz-errorboxfixer.sp | 89 + sourcemod/scripting/gokz-global.sp | 740 ++++++++ sourcemod/scripting/gokz-global/api.sp | 142 ++ sourcemod/scripting/gokz-global/ban_player.sp | 42 + sourcemod/scripting/gokz-global/commands.sp | 169 ++ sourcemod/scripting/gokz-global/maptop_menu.sp | 249 +++ sourcemod/scripting/gokz-global/points.sp | 147 ++ sourcemod/scripting/gokz-global/print_records.sp | 190 ++ sourcemod/scripting/gokz-global/send_run.sp | 143 ++ sourcemod/scripting/gokz-goto.sp | 231 +++ sourcemod/scripting/gokz-hud.sp | 334 ++++ sourcemod/scripting/gokz-hud/commands.sp | 116 ++ sourcemod/scripting/gokz-hud/hide_weapon.sp | 30 + sourcemod/scripting/gokz-hud/info_panel.sp | 307 ++++ sourcemod/scripting/gokz-hud/menu.sp | 96 + sourcemod/scripting/gokz-hud/natives.sp | 33 + sourcemod/scripting/gokz-hud/options.sp | 190 ++ sourcemod/scripting/gokz-hud/options_menu.sp | 181 ++ sourcemod/scripting/gokz-hud/racing_text.sp | 167 ++ sourcemod/scripting/gokz-hud/spectate_text.sp | 119 ++ sourcemod/scripting/gokz-hud/speed_text.sp | 141 ++ sourcemod/scripting/gokz-hud/timer_text.sp | 135 ++ sourcemod/scripting/gokz-hud/tp_menu.sp | 415 +++++ sourcemod/scripting/gokz-jumpbeam.sp | 325 ++++ sourcemod/scripting/gokz-jumpstats.sp | 216 +++ sourcemod/scripting/gokz-jumpstats/api.sp | 78 + sourcemod/scripting/gokz-jumpstats/commands.sp | 28 + .../scripting/gokz-jumpstats/distance_tiers.sp | 118 ++ .../scripting/gokz-jumpstats/jump_reporting.sp | 508 ++++++ .../scripting/gokz-jumpstats/jump_tracking.sp | 1624 +++++++++++++++++ .../scripting/gokz-jumpstats/jump_validating.sp | 82 + sourcemod/scripting/gokz-jumpstats/options.sp | 86 + sourcemod/scripting/gokz-jumpstats/options_menu.sp | 145 ++ sourcemod/scripting/gokz-localdb.sp | 188 ++ sourcemod/scripting/gokz-localdb/api.sp | 126 ++ sourcemod/scripting/gokz-localdb/commands.sp | 199 ++ sourcemod/scripting/gokz-localdb/db/cache_js.sp | 67 + .../scripting/gokz-localdb/db/create_tables.sp | 36 + sourcemod/scripting/gokz-localdb/db/helpers.sp | 18 + sourcemod/scripting/gokz-localdb/db/save_js.sp | 291 +++ sourcemod/scripting/gokz-localdb/db/save_time.sp | 83 + sourcemod/scripting/gokz-localdb/db/set_cheater.sp | 64 + .../scripting/gokz-localdb/db/setup_client.sp | 99 + .../scripting/gokz-localdb/db/setup_database.sp | 34 + sourcemod/scripting/gokz-localdb/db/setup_map.sp | 71 + .../scripting/gokz-localdb/db/setup_map_courses.sp | 45 + sourcemod/scripting/gokz-localdb/db/sql.sp | 406 +++++ sourcemod/scripting/gokz-localdb/db/timer_setup.sp | 167 ++ sourcemod/scripting/gokz-localdb/options.sp | 90 + sourcemod/scripting/gokz-localranks.sp | 263 +++ sourcemod/scripting/gokz-localranks/api.sp | 120 ++ sourcemod/scripting/gokz-localranks/commands.sp | 506 ++++++ .../scripting/gokz-localranks/db/cache_pbs.sp | 62 + .../scripting/gokz-localranks/db/cache_records.sp | 54 + .../scripting/gokz-localranks/db/create_tables.sp | 27 + .../scripting/gokz-localranks/db/display_js.sp | 325 ++++ .../scripting/gokz-localranks/db/get_completion.sp | 155 ++ sourcemod/scripting/gokz-localranks/db/helpers.sp | 91 + sourcemod/scripting/gokz-localranks/db/js_top.sp | 286 +++ sourcemod/scripting/gokz-localranks/db/map_top.sp | 388 ++++ .../scripting/gokz-localranks/db/player_top.sp | 165 ++ .../scripting/gokz-localranks/db/print_average.sp | 152 ++ sourcemod/scripting/gokz-localranks/db/print_js.sp | 108 ++ .../scripting/gokz-localranks/db/print_pbs.sp | 266 +++ .../scripting/gokz-localranks/db/print_records.sp | 173 ++ .../gokz-localranks/db/process_new_time.sp | 157 ++ .../scripting/gokz-localranks/db/recent_records.sp | 171 ++ sourcemod/scripting/gokz-localranks/db/sql.sp | 411 +++++ .../gokz-localranks/db/update_ranked_map_pool.sp | 104 ++ sourcemod/scripting/gokz-localranks/misc.sp | 319 ++++ sourcemod/scripting/gokz-measure.sp | 82 + sourcemod/scripting/gokz-measure/commands.sp | 49 + sourcemod/scripting/gokz-measure/measure_menu.sp | 82 + sourcemod/scripting/gokz-measure/measurer.sp | 231 +++ sourcemod/scripting/gokz-mode-kztimer.sp | 709 ++++++++ sourcemod/scripting/gokz-mode-simplekz.sp | 846 +++++++++ sourcemod/scripting/gokz-mode-vanilla.sp | 291 +++ sourcemod/scripting/gokz-momsurffix.sp | 724 ++++++++ sourcemod/scripting/gokz-paint.sp | 410 +++++ sourcemod/scripting/gokz-pistol.sp | 303 +++ sourcemod/scripting/gokz-playermodels.sp | 198 ++ sourcemod/scripting/gokz-profile.sp | 396 ++++ sourcemod/scripting/gokz-profile/options.sp | 128 ++ sourcemod/scripting/gokz-profile/profile.sp | 222 +++ sourcemod/scripting/gokz-quiet.sp | 151 ++ sourcemod/scripting/gokz-quiet/ambient.sp | 100 + sourcemod/scripting/gokz-quiet/falldamage.sp | 40 + sourcemod/scripting/gokz-quiet/gokz-sounds.sp | 71 + sourcemod/scripting/gokz-quiet/hideplayers.sp | 309 ++++ sourcemod/scripting/gokz-quiet/options.sp | 206 +++ sourcemod/scripting/gokz-quiet/soundscape.sp | 30 + sourcemod/scripting/gokz-racing.sp | 174 ++ sourcemod/scripting/gokz-racing/announce.sp | 229 +++ sourcemod/scripting/gokz-racing/api.sp | 107 ++ sourcemod/scripting/gokz-racing/commands.sp | 47 + sourcemod/scripting/gokz-racing/duel_menu.sp | 534 ++++++ sourcemod/scripting/gokz-racing/race.sp | 221 +++ sourcemod/scripting/gokz-racing/race_menu.sp | 464 +++++ sourcemod/scripting/gokz-racing/racer.sp | 439 +++++ sourcemod/scripting/gokz-replays.sp | 397 ++++ sourcemod/scripting/gokz-replays/api.sp | 78 + sourcemod/scripting/gokz-replays/commands.sp | 55 + sourcemod/scripting/gokz-replays/controls.sp | 224 +++ sourcemod/scripting/gokz-replays/nav.sp | 97 + sourcemod/scripting/gokz-replays/playback.sp | 1501 +++++++++++++++ sourcemod/scripting/gokz-replays/recording.sp | 990 ++++++++++ sourcemod/scripting/gokz-replays/replay_cache.sp | 176 ++ sourcemod/scripting/gokz-replays/replay_menu.sp | 139 ++ sourcemod/scripting/gokz-saveloc.sp | 822 +++++++++ sourcemod/scripting/gokz-slayonend.sp | 190 ++ sourcemod/scripting/gokz-spec.sp | 323 ++++ sourcemod/scripting/gokz-tips.sp | 357 ++++ sourcemod/scripting/gokz-tpanglefix.sp | 277 +++ sourcemod/scripting/include/GlobalAPI.inc | 822 +++++++++ sourcemod/scripting/include/GlobalAPI/iterable.inc | 55 + sourcemod/scripting/include/GlobalAPI/request.inc | 185 ++ .../scripting/include/GlobalAPI/requestdata.inc | 534 ++++++ .../scripting/include/GlobalAPI/responses.inc | 575 ++++++ sourcemod/scripting/include/GlobalAPI/stocks.inc | 67 + sourcemod/scripting/include/SteamWorks.inc | 413 +++++ sourcemod/scripting/include/autoexecconfig.inc | 765 ++++++++ sourcemod/scripting/include/colors.inc | 945 ++++++++++ sourcemod/scripting/include/distbugfix.inc | 261 +++ sourcemod/scripting/include/gamechaos.inc | 20 + sourcemod/scripting/include/gamechaos/arrays.inc | 52 + sourcemod/scripting/include/gamechaos/client.inc | 300 +++ sourcemod/scripting/include/gamechaos/debug.inc | 19 + .../scripting/include/gamechaos/isvalidclient.inc | 16 + .../scripting/include/gamechaos/kreedzclimbing.inc | 226 +++ sourcemod/scripting/include/gamechaos/maths.inc | 362 ++++ sourcemod/scripting/include/gamechaos/misc.inc | 245 +++ sourcemod/scripting/include/gamechaos/strings.inc | 367 ++++ sourcemod/scripting/include/gamechaos/tempents.inc | 62 + sourcemod/scripting/include/gamechaos/tracing.inc | 242 +++ sourcemod/scripting/include/gamechaos/vectors.inc | 66 + sourcemod/scripting/include/glib/addressutils.inc | 54 + sourcemod/scripting/include/glib/assertutils.inc | 61 + sourcemod/scripting/include/glib/memutils.inc | 232 +++ sourcemod/scripting/include/gokz.inc | 1097 +++++++++++ sourcemod/scripting/include/gokz/anticheat.inc | 168 ++ sourcemod/scripting/include/gokz/chat.inc | 45 + sourcemod/scripting/include/gokz/core.inc | 1920 ++++++++++++++++++++ sourcemod/scripting/include/gokz/global.inc | 317 ++++ sourcemod/scripting/include/gokz/hud.inc | 468 +++++ sourcemod/scripting/include/gokz/jumpbeam.inc | 148 ++ sourcemod/scripting/include/gokz/jumpstats.inc | 442 +++++ sourcemod/scripting/include/gokz/kzplayer.inc | 584 ++++++ sourcemod/scripting/include/gokz/localdb.inc | 353 ++++ sourcemod/scripting/include/gokz/localranks.inc | 176 ++ sourcemod/scripting/include/gokz/momsurffix.inc | 23 + sourcemod/scripting/include/gokz/paint.inc | 114 ++ sourcemod/scripting/include/gokz/pistol.inc | 93 + sourcemod/scripting/include/gokz/profile.inc | 291 +++ sourcemod/scripting/include/gokz/quiet.inc | 205 +++ sourcemod/scripting/include/gokz/racing.inc | 189 ++ sourcemod/scripting/include/gokz/replays.inc | 275 +++ sourcemod/scripting/include/gokz/slayonend.inc | 43 + sourcemod/scripting/include/gokz/tips.inc | 59 + sourcemod/scripting/include/gokz/tpanglefix.inc | 40 + sourcemod/scripting/include/gokz/version.inc | 12 + sourcemod/scripting/include/json.inc | 473 +++++ .../scripting/include/json/decode_helpers.inc | 312 ++++ sourcemod/scripting/include/json/definitions.inc | 103 ++ .../scripting/include/json/encode_helpers.inc | 164 ++ .../scripting/include/json/helpers/decode.inc | 502 +++++ .../scripting/include/json/helpers/encode.inc | 200 ++ .../scripting/include/json/helpers/string.inc | 133 ++ sourcemod/scripting/include/json/object.inc | 1014 +++++++++++ .../scripting/include/json/string_helpers.inc | 77 + sourcemod/scripting/include/movement.inc | 530 ++++++ sourcemod/scripting/include/movementapi.inc | 663 +++++++ sourcemod/scripting/include/smjansson.inc | 1328 ++++++++++++++ sourcemod/scripting/include/sourcebanspp.inc | 106 ++ sourcemod/scripting/include/sourcemod-colors.inc | 921 ++++++++++ sourcemod/scripting/include/updater.inc | 97 + sourcemod/scripting/momsurffix/baseplayer.sp | 189 ++ sourcemod/scripting/momsurffix/gamemovement.sp | 411 +++++ sourcemod/scripting/momsurffix/gametrace.sp | 480 +++++ sourcemod/scripting/momsurffix/utils.sp | 279 +++ 209 files changed, 60469 insertions(+) create mode 100644 sourcemod/scripting/distbugfix.sp create mode 100644 sourcemod/scripting/distbugfix/clientprefs.sp create mode 100644 sourcemod/scripting/gokz-anticheat.sp create mode 100644 sourcemod/scripting/gokz-anticheat/api.sp create mode 100644 sourcemod/scripting/gokz-anticheat/bhop_tracking.sp create mode 100644 sourcemod/scripting/gokz-anticheat/commands.sp create mode 100644 sourcemod/scripting/gokz-chat.sp create mode 100644 sourcemod/scripting/gokz-core.sp create mode 100644 sourcemod/scripting/gokz-core/commands.sp create mode 100644 sourcemod/scripting/gokz-core/demofix.sp create mode 100644 sourcemod/scripting/gokz-core/forwards.sp create mode 100644 sourcemod/scripting/gokz-core/map/buttons.sp create mode 100644 sourcemod/scripting/gokz-core/map/end.sp create mode 100644 sourcemod/scripting/gokz-core/map/mapfile.sp create mode 100644 sourcemod/scripting/gokz-core/map/prefix.sp create mode 100644 sourcemod/scripting/gokz-core/map/starts.sp create mode 100644 sourcemod/scripting/gokz-core/map/triggers.sp create mode 100644 sourcemod/scripting/gokz-core/map/zones.sp create mode 100644 sourcemod/scripting/gokz-core/menus/mode_menu.sp create mode 100644 sourcemod/scripting/gokz-core/menus/options_menu.sp create mode 100644 sourcemod/scripting/gokz-core/misc.sp create mode 100644 sourcemod/scripting/gokz-core/modes.sp create mode 100644 sourcemod/scripting/gokz-core/natives.sp create mode 100644 sourcemod/scripting/gokz-core/options.sp create mode 100644 sourcemod/scripting/gokz-core/teamnumfix.sp create mode 100644 sourcemod/scripting/gokz-core/teleports.sp create mode 100644 sourcemod/scripting/gokz-core/timer/pause.sp create mode 100644 sourcemod/scripting/gokz-core/timer/timer.sp create mode 100644 sourcemod/scripting/gokz-core/timer/virtual_buttons.sp create mode 100644 sourcemod/scripting/gokz-core/triggerfix.sp create mode 100644 sourcemod/scripting/gokz-errorboxfixer.sp create mode 100644 sourcemod/scripting/gokz-global.sp create mode 100644 sourcemod/scripting/gokz-global/api.sp create mode 100644 sourcemod/scripting/gokz-global/ban_player.sp create mode 100644 sourcemod/scripting/gokz-global/commands.sp create mode 100644 sourcemod/scripting/gokz-global/maptop_menu.sp create mode 100644 sourcemod/scripting/gokz-global/points.sp create mode 100644 sourcemod/scripting/gokz-global/print_records.sp create mode 100644 sourcemod/scripting/gokz-global/send_run.sp create mode 100644 sourcemod/scripting/gokz-goto.sp create mode 100644 sourcemod/scripting/gokz-hud.sp create mode 100644 sourcemod/scripting/gokz-hud/commands.sp create mode 100644 sourcemod/scripting/gokz-hud/hide_weapon.sp create mode 100644 sourcemod/scripting/gokz-hud/info_panel.sp create mode 100644 sourcemod/scripting/gokz-hud/menu.sp create mode 100644 sourcemod/scripting/gokz-hud/natives.sp create mode 100644 sourcemod/scripting/gokz-hud/options.sp create mode 100644 sourcemod/scripting/gokz-hud/options_menu.sp create mode 100644 sourcemod/scripting/gokz-hud/racing_text.sp create mode 100644 sourcemod/scripting/gokz-hud/spectate_text.sp create mode 100644 sourcemod/scripting/gokz-hud/speed_text.sp create mode 100644 sourcemod/scripting/gokz-hud/timer_text.sp create mode 100644 sourcemod/scripting/gokz-hud/tp_menu.sp create mode 100644 sourcemod/scripting/gokz-jumpbeam.sp create mode 100644 sourcemod/scripting/gokz-jumpstats.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/api.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/commands.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/distance_tiers.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/jump_reporting.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/jump_tracking.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/jump_validating.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/options.sp create mode 100644 sourcemod/scripting/gokz-jumpstats/options_menu.sp create mode 100644 sourcemod/scripting/gokz-localdb.sp create mode 100644 sourcemod/scripting/gokz-localdb/api.sp create mode 100644 sourcemod/scripting/gokz-localdb/commands.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/cache_js.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/create_tables.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/helpers.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/save_js.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/save_time.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/set_cheater.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/setup_client.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/setup_database.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/setup_map.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/sql.sp create mode 100644 sourcemod/scripting/gokz-localdb/db/timer_setup.sp create mode 100644 sourcemod/scripting/gokz-localdb/options.sp create mode 100644 sourcemod/scripting/gokz-localranks.sp create mode 100644 sourcemod/scripting/gokz-localranks/api.sp create mode 100644 sourcemod/scripting/gokz-localranks/commands.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/cache_pbs.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/cache_records.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/create_tables.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/display_js.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/get_completion.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/helpers.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/js_top.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/map_top.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/player_top.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/print_average.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/print_js.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/print_pbs.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/print_records.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/process_new_time.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/recent_records.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/sql.sp create mode 100644 sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp create mode 100644 sourcemod/scripting/gokz-localranks/misc.sp create mode 100644 sourcemod/scripting/gokz-measure.sp create mode 100644 sourcemod/scripting/gokz-measure/commands.sp create mode 100644 sourcemod/scripting/gokz-measure/measure_menu.sp create mode 100644 sourcemod/scripting/gokz-measure/measurer.sp create mode 100644 sourcemod/scripting/gokz-mode-kztimer.sp create mode 100644 sourcemod/scripting/gokz-mode-simplekz.sp create mode 100644 sourcemod/scripting/gokz-mode-vanilla.sp create mode 100644 sourcemod/scripting/gokz-momsurffix.sp create mode 100644 sourcemod/scripting/gokz-paint.sp create mode 100644 sourcemod/scripting/gokz-pistol.sp create mode 100644 sourcemod/scripting/gokz-playermodels.sp create mode 100644 sourcemod/scripting/gokz-profile.sp create mode 100644 sourcemod/scripting/gokz-profile/options.sp create mode 100644 sourcemod/scripting/gokz-profile/profile.sp create mode 100644 sourcemod/scripting/gokz-quiet.sp create mode 100644 sourcemod/scripting/gokz-quiet/ambient.sp create mode 100644 sourcemod/scripting/gokz-quiet/falldamage.sp create mode 100644 sourcemod/scripting/gokz-quiet/gokz-sounds.sp create mode 100644 sourcemod/scripting/gokz-quiet/hideplayers.sp create mode 100644 sourcemod/scripting/gokz-quiet/options.sp create mode 100644 sourcemod/scripting/gokz-quiet/soundscape.sp create mode 100644 sourcemod/scripting/gokz-racing.sp create mode 100644 sourcemod/scripting/gokz-racing/announce.sp create mode 100644 sourcemod/scripting/gokz-racing/api.sp create mode 100644 sourcemod/scripting/gokz-racing/commands.sp create mode 100644 sourcemod/scripting/gokz-racing/duel_menu.sp create mode 100644 sourcemod/scripting/gokz-racing/race.sp create mode 100644 sourcemod/scripting/gokz-racing/race_menu.sp create mode 100644 sourcemod/scripting/gokz-racing/racer.sp create mode 100644 sourcemod/scripting/gokz-replays.sp create mode 100644 sourcemod/scripting/gokz-replays/api.sp create mode 100644 sourcemod/scripting/gokz-replays/commands.sp create mode 100644 sourcemod/scripting/gokz-replays/controls.sp create mode 100644 sourcemod/scripting/gokz-replays/nav.sp create mode 100644 sourcemod/scripting/gokz-replays/playback.sp create mode 100644 sourcemod/scripting/gokz-replays/recording.sp create mode 100644 sourcemod/scripting/gokz-replays/replay_cache.sp create mode 100644 sourcemod/scripting/gokz-replays/replay_menu.sp create mode 100644 sourcemod/scripting/gokz-saveloc.sp create mode 100644 sourcemod/scripting/gokz-slayonend.sp create mode 100644 sourcemod/scripting/gokz-spec.sp create mode 100644 sourcemod/scripting/gokz-tips.sp create mode 100644 sourcemod/scripting/gokz-tpanglefix.sp create mode 100644 sourcemod/scripting/include/GlobalAPI.inc create mode 100644 sourcemod/scripting/include/GlobalAPI/iterable.inc create mode 100644 sourcemod/scripting/include/GlobalAPI/request.inc create mode 100644 sourcemod/scripting/include/GlobalAPI/requestdata.inc create mode 100644 sourcemod/scripting/include/GlobalAPI/responses.inc create mode 100644 sourcemod/scripting/include/GlobalAPI/stocks.inc create mode 100644 sourcemod/scripting/include/SteamWorks.inc create mode 100644 sourcemod/scripting/include/autoexecconfig.inc create mode 100644 sourcemod/scripting/include/colors.inc create mode 100644 sourcemod/scripting/include/distbugfix.inc create mode 100644 sourcemod/scripting/include/gamechaos.inc create mode 100644 sourcemod/scripting/include/gamechaos/arrays.inc create mode 100644 sourcemod/scripting/include/gamechaos/client.inc create mode 100644 sourcemod/scripting/include/gamechaos/debug.inc create mode 100644 sourcemod/scripting/include/gamechaos/isvalidclient.inc create mode 100644 sourcemod/scripting/include/gamechaos/kreedzclimbing.inc create mode 100644 sourcemod/scripting/include/gamechaos/maths.inc create mode 100644 sourcemod/scripting/include/gamechaos/misc.inc create mode 100644 sourcemod/scripting/include/gamechaos/strings.inc create mode 100644 sourcemod/scripting/include/gamechaos/tempents.inc create mode 100644 sourcemod/scripting/include/gamechaos/tracing.inc create mode 100644 sourcemod/scripting/include/gamechaos/vectors.inc create mode 100644 sourcemod/scripting/include/glib/addressutils.inc create mode 100644 sourcemod/scripting/include/glib/assertutils.inc create mode 100644 sourcemod/scripting/include/glib/memutils.inc create mode 100644 sourcemod/scripting/include/gokz.inc create mode 100644 sourcemod/scripting/include/gokz/anticheat.inc create mode 100644 sourcemod/scripting/include/gokz/chat.inc create mode 100644 sourcemod/scripting/include/gokz/core.inc create mode 100644 sourcemod/scripting/include/gokz/global.inc create mode 100644 sourcemod/scripting/include/gokz/hud.inc create mode 100644 sourcemod/scripting/include/gokz/jumpbeam.inc create mode 100644 sourcemod/scripting/include/gokz/jumpstats.inc create mode 100644 sourcemod/scripting/include/gokz/kzplayer.inc create mode 100644 sourcemod/scripting/include/gokz/localdb.inc create mode 100644 sourcemod/scripting/include/gokz/localranks.inc create mode 100644 sourcemod/scripting/include/gokz/momsurffix.inc create mode 100644 sourcemod/scripting/include/gokz/paint.inc create mode 100644 sourcemod/scripting/include/gokz/pistol.inc create mode 100644 sourcemod/scripting/include/gokz/profile.inc create mode 100644 sourcemod/scripting/include/gokz/quiet.inc create mode 100644 sourcemod/scripting/include/gokz/racing.inc create mode 100644 sourcemod/scripting/include/gokz/replays.inc create mode 100644 sourcemod/scripting/include/gokz/slayonend.inc create mode 100644 sourcemod/scripting/include/gokz/tips.inc create mode 100644 sourcemod/scripting/include/gokz/tpanglefix.inc create mode 100644 sourcemod/scripting/include/gokz/version.inc create mode 100644 sourcemod/scripting/include/json.inc create mode 100644 sourcemod/scripting/include/json/decode_helpers.inc create mode 100644 sourcemod/scripting/include/json/definitions.inc create mode 100644 sourcemod/scripting/include/json/encode_helpers.inc create mode 100644 sourcemod/scripting/include/json/helpers/decode.inc create mode 100644 sourcemod/scripting/include/json/helpers/encode.inc create mode 100644 sourcemod/scripting/include/json/helpers/string.inc create mode 100644 sourcemod/scripting/include/json/object.inc create mode 100644 sourcemod/scripting/include/json/string_helpers.inc create mode 100644 sourcemod/scripting/include/movement.inc create mode 100644 sourcemod/scripting/include/movementapi.inc create mode 100644 sourcemod/scripting/include/smjansson.inc create mode 100644 sourcemod/scripting/include/sourcebanspp.inc create mode 100644 sourcemod/scripting/include/sourcemod-colors.inc create mode 100644 sourcemod/scripting/include/updater.inc create mode 100644 sourcemod/scripting/momsurffix/baseplayer.sp create mode 100644 sourcemod/scripting/momsurffix/gamemovement.sp create mode 100644 sourcemod/scripting/momsurffix/gametrace.sp create mode 100644 sourcemod/scripting/momsurffix/utils.sp (limited to 'sourcemod') diff --git a/sourcemod/scripting/distbugfix.sp b/sourcemod/scripting/distbugfix.sp new file mode 100644 index 0000000..d1854b5 --- /dev/null +++ b/sourcemod/scripting/distbugfix.sp @@ -0,0 +1,1592 @@ + +#include +#include +#include +#include +#include + +#pragma newdecls required +#pragma semicolon 1 + +#if defined DEBUG +#define DEBUG_CHAT(%1) PrintToChat(%1); +#define DEBUG_CHATALL(%1) PrintToChatAll(%1); +#define DEBUG_CONSOLE(%1) PrintToConsole(%1); +#else +#define DEBUG_CHAT(%1) +#define DEBUG_CHATALL(%1) +#define DEBUG_CONSOLE(%1) +#endif + +#include +#include + +char g_jumpTypes[JumpType][] = { + "NONE", + "LJ", + "WJ", + "LAJ", + "BH", + "CBH", +}; + +stock char g_szStrafeType[StrafeType][] = { + "$", // STRAFETYPE_OVERLAP + ".", // STRAFETYPE_NONE + + "█", // STRAFETYPE_LEFT + "#", // STRAFETYPE_OVERLAP_LEFT + "H", // STRAFETYPE_NONE_LEFT + + "█", // STRAFETYPE_RIGHT + "#", // STRAFETYPE_OVERLAP_RIGHT + "H", // STRAFETYPE_NONE_RIGHT +}; + +stock char g_szStrafeTypeColour[][] = { + "|", // overlap + "|", // none + "|", // left + "|", // overlap_left + "|", // none_left + "|", // right + "|", // overlap_right + "|", // none_right +}; + +stock bool g_jumpTypePrintable[JumpType] = { + false, // JUMPTYPE_NONE, + + true, // longjump + true, // weirdjump + true, // ladderjump + true, // bunnyhop + true, // ducked bunnyhop +}; + +stock char g_jumpDirString[JumpDir][] = { + "Forwards", + "Backwards", + "Sideways", + "Sideways" +}; + +stock int g_jumpDirForwardButton[JumpDir] = { + IN_FORWARD, + IN_BACK, + IN_MOVELEFT, + IN_MOVERIGHT, +}; + +stock int g_jumpDirLeftButton[JumpDir] = { + IN_MOVELEFT, + IN_MOVERIGHT, + IN_BACK, + IN_FORWARD, +}; + +stock int g_jumpDirRightButton[JumpDir] = { + IN_MOVERIGHT, + IN_MOVELEFT, + IN_FORWARD, + IN_BACK, +}; + +bool g_lateLoad; + +PlayerData g_pd[MAXPLAYERS + 1]; +PlayerData g_failstatPD[MAXPLAYERS + 1]; +int g_beamSprite; + +ConVar g_airaccelerate; +ConVar g_gravity; +ConVar g_maxvelocity; + +ConVar g_jumpRange[JumpType][2]; + +#include "distbugfix/clientprefs.sp" + +public Plugin myinfo = +{ + name = "Distance Bug Fix", + author = "GameChaos", + description = "Fixes longjump distance bug", + version = DISTBUG_VERSION, + url = "https://bitbucket.org/GameChaos/distbug/src" +}; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_lateLoad = late; + + return APLRes_Success; +} + +public void OnPluginStart() +{ + RegConsoleCmd("sm_distbug", Command_Distbug, "Toggle distbug on/off."); + RegConsoleCmd("sm_distbugversion", Command_Distbugversion, "Print distbug version."); + + RegConsoleCmd("sm_distbugbeam", CommandBeam, "Toggle jump beam."); + RegConsoleCmd("sm_distbugveerbeam", CommandVeerbeam, "Toggle veer beam."); + RegConsoleCmd("sm_distbughudgraph", CommandHudgraph, "Toggle hud strafe graph."); + RegConsoleCmd("sm_strafestats", CommandStrafestats, "Toggle distbug strafestats."); + RegConsoleCmd("sm_distbugstrafegraph", CommandStrafegraph, "Toggle console strafe graph."); + RegConsoleCmd("sm_distbugadvchat", CommandAdvchat, "Toggle advanced chat stats."); + RegConsoleCmd("sm_distbughelp", CommandHelp, "Distbug command list."); + + g_airaccelerate = FindConVar("sv_airaccelerate"); + g_gravity = FindConVar("sv_gravity"); + g_maxvelocity = FindConVar("sv_maxvelocity"); + + g_jumpRange[JUMPTYPE_LJ][0] = CreateConVar("distbug_lj_min_dist", "210.0"); + g_jumpRange[JUMPTYPE_LJ][1] = CreateConVar("distbug_lj_max_dist", "310.0"); + + g_jumpRange[JUMPTYPE_WJ][0] = CreateConVar("distbug_wj_min_dist", "210.0"); + g_jumpRange[JUMPTYPE_WJ][1] = CreateConVar("distbug_wj_max_dist", "390.0"); + + g_jumpRange[JUMPTYPE_LAJ][0] = CreateConVar("distbug_laj_min_dist", "70.0"); + g_jumpRange[JUMPTYPE_LAJ][1] = CreateConVar("distbug_laj_max_dist", "250.0"); + + g_jumpRange[JUMPTYPE_BH][0] = CreateConVar("distbug_bh_min_dist", "210.0"); + g_jumpRange[JUMPTYPE_BH][1] = CreateConVar("distbug_bh_max_dist", "390.0"); + + g_jumpRange[JUMPTYPE_CBH][0] = CreateConVar("distbug_cbh_min_dist", "200.0"); + g_jumpRange[JUMPTYPE_CBH][1] = CreateConVar("distbug_cbh_max_dist", "390.0"); + + AutoExecConfig(.name = DISTBUG_CONFIG_NAME); + + HookEvent("player_jump", Event_PlayerJump); + + OnPluginStart_Clientprefs(); + if (g_lateLoad) + { + for (int client = 0; client <= MaxClients; client++) + { + if (GCIsValidClient(client)) + { + OnClientPutInServer(client); + OnClientCookiesCached(client); + } + } + } +} + +public void OnMapStart() +{ + g_beamSprite = PrecacheModel("materials/sprites/laserbeam.vmt"); +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_PostThinkPost, PlayerPostThink); + g_pd[client].tickCount = 0; +} + +public void OnClientCookiesCached(int client) +{ + OnClientCookiesCached_Clientprefs(client); +} + +public void Event_PlayerJump(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (!IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED)) + { + return; + } + + if (GCIsValidClient(client, true)) + { + bool duckbhop = !!(g_pd[client].flags & FL_DUCKING); + float groundOffset = g_pd[client].position[2] - g_pd[client].lastGroundPos[2]; + JumpType jumpType = JUMPTYPE_NONE; + if (g_pd[client].framesOnGround <= MAX_BHOP_FRAMES) + { + if (g_pd[client].lastGroundPosWalkedOff && groundOffset < 0.0) + { + jumpType = JUMPTYPE_WJ; + } + else + { + if (duckbhop) + { + jumpType = JUMPTYPE_CBH; + } + else + { + jumpType = JUMPTYPE_BH; + } + } + } + else + { + jumpType = JUMPTYPE_LJ; + } + + if (jumpType != JUMPTYPE_NONE) + { + OnPlayerJumped(client, g_pd[client], jumpType); + } + + g_pd[client].lastGroundPos = g_pd[client].lastPosition; + g_pd[client].lastGroundPosWalkedOff = false; + } +} + +public Action Command_Distbugversion(int client, int args) +{ + ReplyToCommand(client, "Distbugfix version: %s", DISTBUG_VERSION); + return Plugin_Handled; +} + +public Action Command_Distbug(int client, int args) +{ + ToggleSetting(client, SETTINGS_DISTBUG_ENABLED); + CPrintToChat(client, "%s Distbug has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandBeam(int client, int args) +{ + ToggleSetting(client, SETTINGS_SHOW_JUMP_BEAM); + CPrintToChat(client, "%s Jump beam has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_SHOW_JUMP_BEAM) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandVeerbeam(int client, int args) +{ + ToggleSetting(client, SETTINGS_SHOW_VEER_BEAM); + CPrintToChat(client, "%s Veer beam has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_SHOW_VEER_BEAM) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandHudgraph(int client, int args) +{ + ToggleSetting(client, SETTINGS_SHOW_HUD_GRAPH); + CPrintToChat(client, "%s Hud stats have been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_SHOW_HUD_GRAPH) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandStrafestats(int client, int args) +{ + ToggleSetting(client, SETTINGS_DISABLE_STRAFE_STATS); + CPrintToChat(client, "%s Strafe stats have been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_STATS) ? "disabled." : "enabled."); + + return Plugin_Handled; +} + +public Action CommandStrafegraph(int client, int args) +{ + ToggleSetting(client, SETTINGS_DISABLE_STRAFE_GRAPH); + CPrintToChat(client, "%s Console strafe graph has been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_GRAPH) ? "disabled." : "enabled."); + + return Plugin_Handled; +} + +public Action CommandAdvchat(int client, int args) +{ + ToggleSetting(client, SETTINGS_ADV_CHAT_STATS); + CPrintToChat(client, "%s Advanced chat stats have been %s", CHAT_PREFIX, + IsSettingEnabled(client, SETTINGS_ADV_CHAT_STATS) ? "enabled." : "disabled."); + + return Plugin_Handled; +} + +public Action CommandHelp(int client, int args) +{ + CPrintToChat(client, "%s Look in the console for a list of distbug commands!", CHAT_PREFIX); + PrintToConsole(client, "%s", "Distbug command list:\n" ...\ + "sm_distbug - Toggle distbug on/off.\n" ...\ + "sm_distbugversion - Print distbug version.\n" ...\ + "sm_distbugbeam - Toggle jump beam.\n" ...\ + "sm_distbugveerbeam - Toggle veer beam.\n" ...\ + "sm_distbughudgraph - Toggle hud strafe graph.\n" ...\ + "sm_strafestats - Toggle distbug strafestats.\n" ...\ + "sm_distbugstrafegraph - Toggle console strafe graph.\n" ...\ + "sm_distbugadvchat - Toggle advanced chat stats.\n" ...\ + "sm_distbughelp - Distbug command list.\n"); + return Plugin_Handled; +} + +public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3], float angles[3], int& weapon, int& subtype, int& cmdnum, int& tickcount, int& seed, int mouse[2]) +{ + if (!GCIsValidClient(client, true)) + { + return Plugin_Continue; + } + + if (!IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED)) + { + return Plugin_Continue; + } + + g_pd[client].lastSidemove = g_pd[client].sidemove; + g_pd[client].lastForwardmove = g_pd[client].forwardmove; + g_pd[client].sidemove = vel[1]; + g_pd[client].forwardmove = vel[0]; + + return Plugin_Continue; +} + +public void PlayerPostThink(int client) +{ + if (!GCIsValidClient(client, true)) + { + return; + } + + int flags = GetEntityFlags(client); + g_pd[client].lastButtons = g_pd[client].buttons; + g_pd[client].buttons = GetClientButtons(client); + g_pd[client].lastFlags = g_pd[client].flags; + g_pd[client].flags = flags; + g_pd[client].lastPosition = g_pd[client].position; + g_pd[client].lastAngles = g_pd[client].angles; + g_pd[client].lastVelocity = g_pd[client].velocity; + GetClientAbsOrigin(client, g_pd[client].position); + GetClientEyeAngles(client, g_pd[client].angles); + GCGetClientVelocity(client, g_pd[client].velocity); + GetEntPropVector(client, Prop_Send, "m_vecLadderNormal", g_pd[client].ladderNormal); + + if (flags & FL_ONGROUND) + { + g_pd[client].framesInAir = 0; + g_pd[client].framesOnGround++; + } + else if (g_pd[client].movetype != MOVETYPE_LADDER) + { + g_pd[client].framesInAir++; + g_pd[client].framesOnGround = 0; + } + + g_pd[client].lastMovetype = g_pd[client].movetype; + g_pd[client].movetype = GetEntityMoveType(client); + g_pd[client].stamina = GCGetClientStamina(client); + g_pd[client].lastStamina = g_pd[client].stamina; + g_pd[client].gravity = GetEntityGravity(client); + + // LJ stuff + if (IsSettingEnabled(client, SETTINGS_DISTBUG_ENABLED)) + { + if (g_pd[client].framesInAir == 1) + { + if (!GCVectorsEqual(g_pd[client].lastGroundPos, g_pd[client].lastPosition)) + { + g_pd[client].lastGroundPos = g_pd[client].lastPosition; + g_pd[client].lastGroundPosWalkedOff = true; + } + } + + bool forwardReleased = (g_pd[client].lastButtons & g_jumpDirForwardButton[g_pd[client].jumpDir]) + && !(g_pd[client].buttons & g_jumpDirForwardButton[g_pd[client].jumpDir]); + if (forwardReleased) + { + g_pd[client].fwdReleaseFrame = g_pd[client].tickCount; + } + + if (!g_pd[client].trackingJump + && g_pd[client].movetype == MOVETYPE_WALK + && g_pd[client].lastMovetype == MOVETYPE_LADDER) + { + OnPlayerJumped(client, g_pd[client], JUMPTYPE_LAJ); + } + + if (g_pd[client].framesOnGround == 1) + { + TrackJump(g_pd[client], g_failstatPD[client]); + OnPlayerLanded(client, g_pd[client], g_failstatPD[client]); + } + + if (g_pd[client].trackingJump) + { + TrackJump(g_pd[client], g_failstatPD[client]); + } + } + g_pd[client].tickCount++; + + +#if defined(DEBUG) + SetHudTextParams(-1.0, 0.2, 0.02, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0); + ShowHudText(client, -1, "pos: %f %f %f", g_pd[client].position[0], g_pd[client].position[1], g_pd[client].position[2]); +#endif +} + +bool IsSpectating(int spectator, int target) +{ + if (spectator != target && GCIsValidClient(spectator)) + { + int specMode = GetEntProp(spectator, Prop_Send, "m_iObserverMode"); + if (specMode == 4 || specMode == 5) + { + if (GetEntPropEnt(spectator, Prop_Send, "m_hObserverTarget") == target) + { + return true; + } + } + } + return false; +} + +void ClientAndSpecsPrintChat(int client, const char[] format, any ...) +{ + static char message[1024]; + VFormat(message, sizeof(message), format, 3); + CPrintToChat(client, "%s", message); + + for (int spec = 1; spec <= MaxClients; spec++) + { + if (IsSpectating(spec, client) && IsSettingEnabled(spec, SETTINGS_DISTBUG_ENABLED)) + { + CPrintToChat(spec, "%s", message); + } + } +} + +void ClientAndSpecsPrintConsole(int client, const char[] format, any ...) +{ + static char message[1024]; + VFormat(message, sizeof(message), format, 3); + PrintToConsole(client, "%s", message); + + for (int spec = 1; spec < MAXPLAYERS; spec++) + { + if (IsSpectating(spec, client) && IsSettingEnabled(spec, SETTINGS_DISTBUG_ENABLED)) + { + PrintToConsole(spec, "%s", message); + } + } +} + +void ResetJump(PlayerData pd) +{ + // NOTE: only resets things that need to be reset + for (int i = 0; i < 3; i++) + { + pd.jumpPos[i] = 0.0; + pd.landPos[i] = 0.0; + } + pd.trackingJump = false; + pd.failedJump = false; + pd.jumpGotFailstats = false; + + // Jump data + // pd.jumpType = JUMPTYPE_NONE; + // NOTE: don't reset jumpType or lastJumpType + pd.jumpMaxspeed = 0.0; + pd.jumpSync = 0.0; + pd.jumpEdge = 0.0; + pd.jumpBlockDist = 0.0; + pd.jumpHeight = 0.0; + pd.jumpAirtime = 0; + pd.jumpOverlap = 0; + pd.jumpDeadair = 0; + pd.jumpAirpath = 0.0; + + pd.strafeCount = 0; + for (int i = 0; i < MAX_STRAFES; i++) + { + pd.strafeSync[i] = 0.0; + pd.strafeGain[i] = 0.0; + pd.strafeLoss[i] = 0.0; + pd.strafeMax[i] = 0.0; + pd.strafeAirtime[i] = 0; + pd.strafeOverlap[i] = 0; + pd.strafeDeadair[i] = 0; + pd.strafeAvgGain[i] = 0.0; + pd.strafeAvgEfficiency[i] = 0.0; + pd.strafeAvgEfficiencyCount[i] = 0; + pd.strafeMaxEfficiency[i] = GC_FLOAT_NEGATIVE_INFINITY; + } +} + +bool IsWishspeedMovingLeft(float forwardspeed, float sidespeed, JumpDir jumpDir) +{ + if (jumpDir == JUMPDIR_FORWARDS) + { + return sidespeed < 0.0; + } + else if (jumpDir == JUMPDIR_BACKWARDS) + { + return sidespeed > 0.0; + } + else if (jumpDir == JUMPDIR_LEFT) + { + return forwardspeed < 0.0; + } + // else if (jumpDir == JUMPDIR_RIGHT) + return forwardspeed > 0.0; +} + +bool IsWishspeedMovingRight(float forwardspeed, float sidespeed, JumpDir jumpDir) +{ + if (jumpDir == JUMPDIR_FORWARDS) + { + return sidespeed > 0.0; + } + else if (jumpDir == JUMPDIR_BACKWARDS) + { + return sidespeed < 0.0; + } + else if (jumpDir == JUMPDIR_LEFT) + { + return forwardspeed > 0.0; + } + // else if (jumpDir == JUMPDIR_RIGHT) + return forwardspeed < 0.0; +} + +bool IsNewStrafe(PlayerData pd) +{ + if (pd.jumpDir == JUMPDIR_FORWARDS || pd.jumpDir == JUMPDIR_BACKWARDS) + { + return ((pd.sidemove > 0.0 && pd.lastSidemove <= 0.0) + || (pd.sidemove < 0.0 && pd.lastSidemove >= 0.0)) + && pd.jumpAirtime != 1; + } + // else if (pd.jumpDir == JUMPDIR_LEFT || pd.jumpDir == JUMPDIR_RIGHT) + return ((pd.forwardmove > 0.0 && pd.lastForwardmove <= 0.0) + || (pd.forwardmove < 0.0 && pd.lastForwardmove >= 0.0)) + && pd.jumpAirtime != 1; +} + +void TrackJump(PlayerData pd, PlayerData failstatPD) +{ +#if defined(DEBUG) + SetHudTextParams(-1.0, 0.2, 0.02, 255, 255, 255, 255, 0, 0.0, 0.0, 0.0); + ShowHudText(1, -1, "FOG: %i\njumpAirtime: %i\ntrackingJump: %i", pd.framesOnGround, pd.jumpAirtime, pd.trackingJump); +#endif + + if (pd.framesOnGround > MAX_BHOP_FRAMES + && pd.jumpAirtime && pd.trackingJump) + { + ResetJump(pd); + } + + if (pd.jumpType == JUMPTYPE_NONE + || !g_jumpTypePrintable[pd.jumpType]) + { + pd.trackingJump = false; + return; + } + + if (pd.movetype != MOVETYPE_WALK + && pd.movetype != MOVETYPE_LADDER) + { + ResetJump(pd); + } + + float frametime = GetTickInterval(); + // crusty teleport detection + { + float posDelta[3]; + SubtractVectors(pd.position, pd.lastPosition, posDelta); + + float moveLength = GetVectorLength(posDelta); + // NOTE: 1.73205081 * sv_maxvelocity is the max velocity magnitude you can get. + if (moveLength > g_maxvelocity.FloatValue * 1.73205081 * frametime) + { + ResetJump(pd); + return; + } + } + + int beamIndex = pd.jumpAirtime; + if (beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamX[beamIndex] = pd.position[0]; + pd.jumpBeamY[beamIndex] = pd.position[1]; + pd.jumpBeamColour[beamIndex] = JUMPBEAM_NEUTRAL; + } + pd.jumpAirtime++; + + + float speed = GCGetVectorLength2D(pd.velocity); + if (speed > pd.jumpMaxspeed) + { + pd.jumpMaxspeed = speed; + } + + float lastSpeed = GCGetVectorLength2D(pd.lastVelocity); + if (speed > lastSpeed) + { + pd.jumpSync++; + if (beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamColour[beamIndex] = JUMPBEAM_GAIN; + } + } + else if (speed < lastSpeed && beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamColour[beamIndex] = JUMPBEAM_LOSS; + } + + if (pd.flags & FL_DUCKING && beamIndex < MAX_JUMP_FRAMES) + { + pd.jumpBeamColour[beamIndex] = JUMPBEAM_DUCK; + } + + float height = pd.position[2] - pd.jumpPos[2]; + if (height > pd.jumpHeight) + { + pd.jumpHeight = height; + } + + if (IsOverlapping(pd.buttons, pd.jumpDir)) + { + pd.jumpOverlap++; + } + + if (IsDeadAirtime(pd.buttons, pd.jumpDir)) + { + pd.jumpDeadair++; + } + + // strafestats! + if (pd.strafeCount + 1 < MAX_STRAFES) + { + if (IsNewStrafe(pd)) + { + pd.strafeCount++; + } + + int strafe = pd.strafeCount; + + pd.strafeAirtime[strafe]++; + + if (speed > lastSpeed) + { + pd.strafeSync[strafe] += 1.0; + pd.strafeGain[strafe] += speed - lastSpeed; + } + else if (speed < lastSpeed) + { + pd.strafeLoss[strafe] += lastSpeed - speed; + } + + if (speed > pd.strafeMax[strafe]) + { + pd.strafeMax[strafe] = speed; + } + + if (IsOverlapping(pd.buttons, pd.jumpDir)) + { + pd.strafeOverlap[strafe]++; + } + + if (IsDeadAirtime(pd.buttons, pd.jumpDir)) + { + pd.strafeDeadair[strafe]++; + } + + // efficiency! + { + float maxWishspeed = 30.0; + float airaccelerate = g_airaccelerate.FloatValue; + // NOTE: Assume 250 maxspeed cos this is KZ! + float maxspeed = 250.0; + if (pd.flags & FL_DUCKING) + { + maxspeed *= 0.34; + } + else if (pd.buttons & IN_SPEED) + { + maxspeed *= 0.52; + } + + if (pd.lastStamina > 0) + { + float speedScale = GCFloatClamp(1.0 - pd.lastStamina / 100.0, 0.0, 1.0); + speedScale *= speedScale; + maxspeed *= speedScale; + } + + // calculate zvel 1 tick before pd.lastVelocity and during movement processing + float zvel = pd.lastVelocity[2] + (g_gravity.FloatValue * frametime * 0.5 * pd.gravity); + if (zvel > 0.0 && zvel <= 140.0) + { + maxspeed *= 0.25; + } + + float yawdiff = FloatAbs(GCNormaliseYaw(pd.angles[1] - pd.lastAngles[1])); + float perfectYawDiff = yawdiff; + if (lastSpeed > 0.0) + { + float accelspeed = airaccelerate * maxspeed * frametime; + if (accelspeed > maxWishspeed) + { + accelspeed = maxWishspeed; + } + if (lastSpeed >= maxWishspeed) + { + perfectYawDiff = RadToDeg(ArcSine(accelspeed / lastSpeed)); + } + else + { + perfectYawDiff = 0.0; + } + } + float efficiency = 100.0; + if (perfectYawDiff != 0.0) + { + efficiency = (yawdiff - perfectYawDiff) / perfectYawDiff * 100.0 + 100.0; + } + + pd.strafeAvgEfficiency[strafe] += efficiency; + pd.strafeAvgEfficiencyCount[strafe]++; + if (efficiency > pd.strafeMaxEfficiency[strafe]) + { + pd.strafeMaxEfficiency[strafe] = efficiency; + } + + DEBUG_CONSOLE(1, "%i\t%f\t%f\t%f\t%f\t%f", strafe, (yawdiff - perfectYawDiff), pd.sidemove, yawdiff, perfectYawDiff, speed) + } + } + + // strafe type and mouse graph + if (pd.jumpAirtime - 1 < MAX_JUMP_FRAMES) + { + StrafeType strafeType = STRAFETYPE_NONE; + + bool moveLeft = !!(pd.buttons & g_jumpDirLeftButton[pd.jumpDir]); + bool moveRight = !!(pd.buttons & g_jumpDirRightButton[pd.jumpDir]); + + bool velLeft = IsWishspeedMovingLeft(pd.forwardmove, pd.sidemove, pd.jumpDir); + bool velRight = IsWishspeedMovingRight(pd.forwardmove, pd.sidemove, pd.jumpDir); + bool velIsZero = !velLeft && !velRight; + + if (moveLeft && !moveRight && velLeft) + { + strafeType = STRAFETYPE_LEFT; + } + else if (moveRight && !moveLeft && velRight) + { + strafeType = STRAFETYPE_RIGHT; + } + else if (moveRight && !moveLeft && velRight) + { + strafeType = STRAFETYPE_LEFT; + } + else if (moveRight && moveLeft && velIsZero) + { + strafeType = STRAFETYPE_OVERLAP; + } + else if (moveRight && moveLeft && velLeft) + { + strafeType = STRAFETYPE_OVERLAP_LEFT; + } + else if (moveRight && moveLeft && velRight) + { + strafeType = STRAFETYPE_OVERLAP_RIGHT; + } + else if (!moveRight && !moveLeft && velIsZero) + { + strafeType = STRAFETYPE_NONE; + } + else if (!moveRight && !moveLeft && velLeft) + { + strafeType = STRAFETYPE_NONE_LEFT; + } + else if (!moveRight && !moveLeft && velRight) + { + strafeType = STRAFETYPE_NONE_RIGHT; + } + + pd.strafeGraph[pd.jumpAirtime - 1] = strafeType; + float yawDiff = GCNormaliseYaw(pd.angles[1] - pd.lastAngles[1]); + // Offset index by 2 to align mouse movement with button presses. + int yawIndex = GCIntMax(pd.jumpAirtime - 2, 0); + pd.mouseGraph[yawIndex] = yawDiff; + } + // check for failstat after jump tracking is done + float duckedPos[3]; + duckedPos = pd.position; + if (!(pd.flags & FL_DUCKING)) + { + duckedPos[2] += 9.0; + } + + // only save failed jump if we're at the fail threshold + if ((pd.position[2] < pd.jumpPos[2]) + && (pd.position[2] > pd.jumpPos[2] + (pd.velocity[2] * frametime))) + { + pd.jumpGotFailstats = true; + failstatPD = pd; + } + + // airpath. + // NOTE: Track airpath after failstatPD has been saved, so + // we don't track the last frame of failstats. That should + // happen inside of FinishTrackingJump, because we need the real landing position. + if (!pd.framesOnGround) + { + // NOTE: there's a special case for landing frame. + float delta[3]; + SubtractVectors(pd.position, pd.lastPosition, delta); + pd.jumpAirpath += GCGetVectorLength2D(delta); + } +} + +void OnPlayerFailstat(int client, PlayerData pd) +{ + if (!pd.jumpGotFailstats) + { + ResetJump(pd); + return; + } + + pd.failedJump = true; + + // undo half the gravity + float gravity = g_gravity.FloatValue * pd.gravity; + float frametime = GetTickInterval(); + float fixedVelocity[3]; + fixedVelocity = pd.velocity; + fixedVelocity[2] += gravity * 0.5 * frametime; + + // fix incorrect distance when ducking / unducking at the right time + float lastPosition[3]; + lastPosition = pd.lastPosition; + bool lastDucking = !!(pd.lastFlags & FL_DUCKING); + bool ducking = !!(pd.flags & FL_DUCKING); + if (!lastDucking && ducking) + { + lastPosition[2] += 9.0; + } + else if (lastDucking && !ducking) + { + lastPosition[2] -= 9.0; + } + + GetRealLandingOrigin(pd.jumpPos[2], lastPosition, fixedVelocity, pd.landPos); + pd.jumpDistance = GCGetVectorDistance2D(pd.jumpPos, pd.landPos); + if (pd.jumpType != JUMPTYPE_LAJ) + { + pd.jumpDistance += 32.0; + } + + FinishTrackingJump(client, pd); + PrintStats(client, pd); + ResetJump(pd); +} + +void OnPlayerJumped(int client, PlayerData pd, JumpType jumpType) +{ + pd.lastJumpType = pd.jumpType; + ResetJump(pd); + pd.jumpType = jumpType; + if (g_jumpTypePrintable[jumpType]) + { + pd.trackingJump = true; + } + + pd.prespeedFog = pd.framesOnGround; + pd.prespeedStamina = pd.stamina; + + // DEBUG_CHAT(1, "jump type: %s last jump type: %s", g_jumpTypes[jumpType], g_jumpTypes[pd.lastJumpType]) + + // jump direction + float speed = GCGetVectorLength2D(pd.velocity); + pd.jumpDir = JUMPDIR_FORWARDS; + // NOTE: Ladderjump pres can be super wild and can generate random + // jump directions, so default to forward for ladderjumps. + if (speed > 50.0 && pd.jumpType != JUMPTYPE_LAJ) + { + float velDir = RadToDeg(ArcTangent2(pd.velocity[1], pd.velocity[0])); + float dir = GCNormaliseYaw(pd.angles[1] - velDir); + + if (GCIsFloatInRange(dir, 45.0, 135.0)) + { + pd.jumpDir = JUMPDIR_RIGHT; + } + if (GCIsFloatInRange(dir, -135.0, -45.0)) + { + pd.jumpDir = JUMPDIR_LEFT; + } + else if (dir > 135.0 || dir < -135.0) + { + pd.jumpDir = JUMPDIR_BACKWARDS; + } + } + + if (jumpType != JUMPTYPE_LAJ) + { + pd.jumpFrame = pd.tickCount; + pd.jumpPos = pd.position; + pd.jumpAngles = pd.angles; + + DEBUG_CHAT(client, "jumppos z: %f", pd.jumpPos[2]) + + pd.jumpPrespeed = GCGetVectorLength2D(pd.velocity); + + pd.jumpGroundZ = pd.jumpPos[2]; + float ground[3]; + if (GCTraceGround(client, pd.jumpPos, ground)) + { + pd.jumpGroundZ = ground[2]; + } + else + { + DEBUG_CHATALL("AAAAAAAAAAAAA") + } + } + else + { + // NOTE: for ladderjump set prespeed and stamina to values that don't get shown + pd.prespeedFog = -1; + pd.prespeedStamina = 0.0; + pd.jumpFrame = pd.tickCount - 1; + pd.jumpPos = pd.lastPosition; + pd.jumpAngles = pd.lastAngles; + + pd.jumpPrespeed = GCGetVectorLength2D(pd.lastVelocity); + + // find ladder top + + float traceOrigin[3]; + // 10 units is the furthest away from the ladder surface you can get while still being on the ladder + traceOrigin[0] = pd.jumpPos[0] - 10.0 * pd.ladderNormal[0]; + traceOrigin[1] = pd.jumpPos[1] - 10.0 * pd.ladderNormal[1]; + traceOrigin[2] = pd.jumpPos[2] + 400.0 * GetTickInterval(); // ~400 ups is the fastest vertical speed on ladders + + float traceEnd[3]; + traceEnd = traceOrigin; + traceEnd[2] = pd.jumpPos[2] - 400.0 * GetTickInterval(); + + float mins[3]; + GetClientMins(client, mins); + + float maxs[3]; + GetClientMaxs(client, maxs); + + TR_TraceHullFilter(traceOrigin, traceEnd, mins, maxs, CONTENTS_LADDER, GCTraceEntityFilterPlayer); + + pd.jumpGroundZ = pd.jumpPos[2]; + if (TR_DidHit()) + { + float result[3]; + TR_GetEndPosition(result); + pd.jumpGroundZ = result[2]; + } + } +} + +void OnPlayerLanded(int client, PlayerData pd, PlayerData failstatPD) +{ + pd.landedDucked = !!(pd.flags & FL_DUCKING); + + if (!pd.trackingJump + || pd.jumpType == JUMPTYPE_NONE + || !g_jumpTypePrintable[pd.jumpType]) + { + ResetJump(pd); + return; + } + + if (pd.jumpType != JUMPTYPE_LAJ) + { + float roughOffset = pd.position[2] - pd.jumpPos[2]; + if (0.0 < roughOffset > 2.0) + { + ResetJump(pd); + return; + } + } + + { + float landGround[3]; + GCTraceGround(client, pd.position, landGround); + pd.landGroundZ = landGround[2]; + } + + float offsetTolerance = 0.0001; + if (!GCIsRoughlyEqual(pd.jumpGroundZ, pd.landGroundZ, offsetTolerance) && pd.jumpGotFailstats) + { + OnPlayerFailstat(client, failstatPD); + return; + } + + float landOrigin[3]; + float gravity = g_gravity.FloatValue * pd.gravity; + float frametime = GetTickInterval(); + float fixedVelocity[3]; + float airOrigin[3]; + + // fix incorrect landing position + float lastPosition[3]; + lastPosition = pd.lastPosition; + bool lastDucking = !!(pd.lastFlags & FL_DUCKING); + bool ducking = !!(pd.flags & FL_DUCKING); + if (!lastDucking && ducking) + { + lastPosition[2] += 9.0; + } + else if (lastDucking && !ducking) + { + lastPosition[2] -= 9.0; + } + + bool isBugged = pd.lastPosition[2] - pd.landGroundZ < 2.0; + if (isBugged) + { + fixedVelocity = pd.velocity; + // NOTE: The 0.5 here removes half the gravity in a tick, because + // in pmove code half the gravity is applied before movement calculation and the other half after it's finished. + // We're trying to fix a bug that happens in the middle of movement code. + fixedVelocity[2] = pd.lastVelocity[2] - gravity * 0.5 * frametime; + airOrigin = lastPosition; + } + else + { + // NOTE: calculate current frame's z velocity + float tempVel[3]; + tempVel = pd.velocity; + tempVel[2] = pd.lastVelocity[2] - gravity * 0.5 * frametime; + // NOTE: calculate velocity after the current frame. + fixedVelocity = tempVel; + fixedVelocity[2] -= gravity * frametime; + + airOrigin = pd.position; + } + + GetRealLandingOrigin(pd.landGroundZ, airOrigin, fixedVelocity, landOrigin); + pd.landPos = landOrigin; + + pd.jumpDistance = (GCGetVectorDistance2D(pd.jumpPos, pd.landPos)); + if (pd.jumpType != JUMPTYPE_LAJ) + { + pd.jumpDistance += 32.0; + } + + if (GCIsFloatInRange(pd.jumpDistance, + g_jumpRange[pd.jumpType][0].FloatValue, + g_jumpRange[pd.jumpType][1].FloatValue)) + { + FinishTrackingJump(client, pd); + + PrintStats(client, pd); + } + else + { + DEBUG_CHAT(client, "bad jump distance %f", pd.jumpDistance) + } + ResetJump(pd); +} + +void FinishTrackingJump(int client, PlayerData pd) +{ + // finish up stats: + float xAxisVeer = FloatAbs(pd.landPos[0] - pd.jumpPos[0]); + float yAxisVeer = FloatAbs(pd.landPos[1] - pd.jumpPos[1]); + pd.jumpVeer = GCFloatMin(xAxisVeer, yAxisVeer); + + pd.jumpFwdRelease = pd.fwdReleaseFrame - pd.jumpFrame; + pd.jumpSync = (pd.jumpSync / float(pd.jumpAirtime) * 100.0); + + for (int strafe; strafe < pd.strafeCount + 1; strafe++) + { + // average gain + pd.strafeAvgGain[strafe] = (pd.strafeGain[strafe] / pd.strafeAirtime[strafe]); + + // efficiency! + if (pd.strafeAvgEfficiencyCount[strafe]) + { + pd.strafeAvgEfficiency[strafe] /= float(pd.strafeAvgEfficiencyCount[strafe]); + } + else + { + pd.strafeAvgEfficiency[strafe] = GC_FLOAT_NAN; + } + + // sync + + if (pd.strafeAirtime[strafe] != 0.0) + { + pd.strafeSync[strafe] = (pd.strafeSync[strafe] / float(pd.strafeAirtime[strafe]) * 100.0); + } + else + { + pd.strafeSync[strafe] = 0.0; + } + } + + // airpath! + { + float delta[3]; + SubtractVectors(pd.landPos, pd.lastPosition, delta); + pd.jumpAirpath += GCGetVectorLength2D(delta); + if (pd.jumpType != JUMPTYPE_LAJ) + { + pd.jumpAirpath = (pd.jumpAirpath / (pd.jumpDistance - 32.0)); + } + else + { + pd.jumpAirpath = (pd.jumpAirpath / (pd.jumpDistance)); + } + } + + pd.jumpBlockDist = -1.0; + pd.jumpLandEdge = -9999.9; + pd.jumpEdge = -1.0; + // Calculate block distance and jumpoff edge + if (pd.jumpType != JUMPTYPE_LAJ) + { + int blockAxis = FloatAbs(pd.landPos[1] - pd.jumpPos[1]) > FloatAbs(pd.landPos[0] - pd.jumpPos[0]); + int blockDir = FloatSign(pd.jumpPos[blockAxis] - pd.landPos[blockAxis]); + + float jumpOrigin[3]; + float landOrigin[3]; + jumpOrigin = pd.jumpPos; + landOrigin = pd.landPos; + // move origins 2 units down, so we can touch the side of the lj blocks + jumpOrigin[2] -= 2.0; + landOrigin[2] -= 2.0; + + // extend land origin, so if we fail within 16 units of the block we can still get the block distance. + landOrigin[blockAxis] -= float(blockDir) * 16.0; + + float tempPos[3]; + tempPos = landOrigin; + tempPos[blockAxis] += (jumpOrigin[blockAxis] - landOrigin[blockAxis]) / 2.0; + + float jumpEdge[3]; + GCTraceBlock(tempPos, jumpOrigin, jumpEdge); + + tempPos = jumpOrigin; + tempPos[blockAxis] += (landOrigin[blockAxis] - jumpOrigin[blockAxis]) / 2.0; + + bool block; + float landEdge[3]; + block = GCTraceBlock(tempPos, landOrigin, landEdge); + + if (block) + { + pd.jumpBlockDist = (FloatAbs(landEdge[blockAxis] - jumpEdge[blockAxis]) + 32.0); + pd.jumpLandEdge = ((landEdge[blockAxis] - pd.landPos[blockAxis]) * float(blockDir)); + } + + if (jumpEdge[blockAxis] - tempPos[blockAxis] != 0.0) + { + pd.jumpEdge = FloatAbs(jumpOrigin[blockAxis] - jumpEdge[blockAxis]); + } + } + else + { + int blockAxis = FloatAbs(pd.landPos[1] - pd.jumpPos[1]) > FloatAbs(pd.landPos[0] - pd.jumpPos[0]); + int blockDir = FloatSign(pd.jumpPos[blockAxis] - pd.landPos[blockAxis]); + + // find ladder front + + float traceOrigin[3]; + // 10 units is the furthest away from the ladder surface you can get while still being on the ladder + traceOrigin[0] = pd.jumpPos[0]; + traceOrigin[1] = pd.jumpPos[1]; + traceOrigin[2] = pd.jumpPos[2] - 400.0 * GetTickInterval(); // ~400 ups is the fastest vertical speed on ladders + + // leave enough room to trace the front of the ladder + traceOrigin[blockAxis] += blockDir * 40.0; + + float traceEnd[3]; + traceEnd = traceOrigin; + traceEnd[blockAxis] -= blockDir * 50.0; + + float mins[3]; + GetClientMins(client, mins); + + float maxs[3]; + GetClientMaxs(client, maxs); + maxs[2] = mins[2]; + + TR_TraceHullFilter(traceOrigin, traceEnd, mins, maxs, CONTENTS_LADDER, GCTraceEntityFilterPlayer); + + float jumpEdge[3]; + if (TR_DidHit()) + { + TR_GetEndPosition(jumpEdge); + DEBUG_CHAT(1, "ladder front: %f %f %f", jumpEdge[0], jumpEdge[1], jumpEdge[2]) + + float jumpOrigin[3]; + float landOrigin[3]; + jumpOrigin = pd.jumpPos; + landOrigin = pd.landPos; + // move origins 2 units down, so we can touch the side of the lj blocks + jumpOrigin[2] -= 2.0; + landOrigin[2] -= 2.0; + + // extend land origin, so if we fail within 16 units of the block we can still get the block distance. + landOrigin[blockAxis] -= float(blockDir) * 16.0; + + float tempPos[3]; + tempPos = jumpOrigin; + tempPos[blockAxis] += (landOrigin[blockAxis] - jumpOrigin[blockAxis]) / 2.0; + + float landEdge[3]; + bool land = GCTraceBlock(tempPos, landOrigin, landEdge); + DEBUG_CHAT(1, "tracing from %f %f %f to %f %f %f", tempPos[0], tempPos[1], tempPos[2], landOrigin[0], landOrigin[1], landOrigin[2]) + + if (land) + { + pd.jumpBlockDist = (FloatAbs(landEdge[blockAxis] - jumpEdge[blockAxis])); + pd.jumpLandEdge = ((landEdge[blockAxis] - pd.landPos[blockAxis]) * float(blockDir)); + } + + pd.jumpEdge = FloatAbs(jumpOrigin[blockAxis] - jumpEdge[blockAxis]); + } + } + + // jumpoff angle! + { + float airpathDir[3]; + SubtractVectors(pd.landPos, pd.jumpPos, airpathDir); + NormalizeVector(airpathDir, airpathDir); + + float airpathAngles[3]; + GetVectorAngles(airpathDir, airpathAngles); + float airpathYaw = GCNormaliseYaw(airpathAngles[1]); + + pd.jumpJumpoffAngle = GCNormaliseYaw(airpathYaw - pd.jumpAngles[1]); + } +} + +void PrintStats(int client, PlayerData pd) +{ + // beams! + if (IsSettingEnabled(client, SETTINGS_SHOW_VEER_BEAM)) + { + float beamEnd[3]; + beamEnd[0] = pd.landPos[0]; + beamEnd[1] = pd.jumpPos[1]; + beamEnd[2] = pd.landPos[2]; + float jumpPos[3]; + float landPos[3]; + for (int i = 0; i < 3; i++) + { + jumpPos[i] = pd.jumpPos[i]; + landPos[i] = pd.landPos[i]; + } + + GCTE_SetupBeamPoints(.start = jumpPos, .end = landPos, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = {255, 255, 255, 95}); + TE_SendToClient(client); + + // x axis + GCTE_SetupBeamPoints(.start = jumpPos, .end = beamEnd, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = {255, 0, 255, 95}); + TE_SendToClient(client); + // y axis + GCTE_SetupBeamPoints(.start = landPos, .end = beamEnd, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = {0, 255, 0, 95}); + TE_SendToClient(client); + } + + if (IsSettingEnabled(client, SETTINGS_SHOW_JUMP_BEAM)) + { + float beamPos[3]; + float lastBeamPos[3]; + beamPos[0] = pd.jumpPos[0]; + beamPos[1] = pd.jumpPos[1]; + beamPos[2] = pd.jumpPos[2]; + for (int i = 1; i < pd.jumpAirtime; i++) + { + lastBeamPos = beamPos; + beamPos[0] = pd.jumpBeamX[i]; + beamPos[1] = pd.jumpBeamY[i]; + + int colour[4] = {255, 191, 0, 255}; + if (pd.jumpBeamColour[i] == JUMPBEAM_LOSS) + { + colour = {255, 0, 255, 255}; + } + else if (pd.jumpBeamColour[i] == JUMPBEAM_GAIN) + { + colour = {0, 127, 0, 255}; + } + else if (pd.jumpBeamColour[i] == JUMPBEAM_DUCK) + { + colour = {0, 31, 127, 255}; + } + + GCTE_SetupBeamPoints(.start = lastBeamPos, .end = beamPos, .modelIndex = g_beamSprite, + .life = 5.0, .width = 1.0, .endWidth = 1.0, .colour = colour); + TE_SendToClient(client); + } + } + + char fwdRelease[32] = ""; + if (pd.jumpFwdRelease == 0) + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {gr}0"); + } + else if (GCIntAbs(pd.jumpFwdRelease) > 16) + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {dr}No"); + } + else if (pd.jumpFwdRelease > 0) + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {dr}+%i", pd.jumpFwdRelease); + } + else + { + FormatEx(fwdRelease, sizeof(fwdRelease), "Fwd: {sb}%i", pd.jumpFwdRelease); + } + + char edge[32] = ""; + char chatEdge[32] = ""; + bool hasEdge = false; + if (pd.jumpEdge >= 0.0 && pd.jumpEdge < MAX_EDGE) + { + FormatEx(edge, sizeof(edge), "Edge: %.4f", pd.jumpEdge); + FormatEx(chatEdge, sizeof(chatEdge), "Edge: {l}%.2f{g}", pd.jumpEdge); + hasEdge = true; + } + + char block[32] = ""; + char chatBlock[32] = ""; + bool hasBlock = false; + if (GCIsFloatInRange(pd.jumpBlockDist, + g_jumpRange[pd.jumpType][0].FloatValue, + g_jumpRange[pd.jumpType][1].FloatValue)) + { + FormatEx(block, sizeof(block), "Block: %i", RoundFloat(pd.jumpBlockDist)); + FormatEx(chatBlock, sizeof(chatBlock), "({l}%i{g})", RoundFloat(pd.jumpBlockDist)); + hasBlock = true; + } + + char landEdge[32] = ""; + bool hasLandEdge = false; + if (FloatAbs(pd.jumpLandEdge) < MAX_EDGE) + { + FormatEx(landEdge, sizeof(landEdge), "Land Edge: %.4f", pd.jumpLandEdge); + hasLandEdge = true; + } + + char fog[32]; + bool hasFOG = false; + if (pd.prespeedFog <= MAX_BHOP_FRAMES && pd.prespeedFog >= 0) + { + FormatEx(fog, sizeof(fog), "FOG: %i", pd.prespeedFog); + hasFOG = true; + } + + char stamina[32]; + bool hasStamina = false; + if (pd.prespeedStamina != 0.0) + { + FormatEx(stamina, sizeof(stamina), "Stamina: %.1f", pd.prespeedStamina); + hasStamina = true; + } + + char offset[32]; + bool hasOffset = false; + if (pd.jumpGroundZ != pd.jumpPos[2]) + { + FormatEx(offset, sizeof(offset), "Ground offset: %.4f", pd.jumpPos[2] - pd.jumpGroundZ); + hasOffset = true; + } + + + //ClientAndSpecsPrintChat(client, "%s", chatStats); + + // TODO: remove jump direction from ladderjumps + char consoleStats[1024]; + FormatEx(consoleStats, sizeof(consoleStats), "\n"...CONSOLE_PREFIX..." %s%s: %.5f [%s%s%s%sVeer: %.4f | %s | Sync: %.2f | Max: %.3f]\n"...\ + "[%s%sPre: %.4f | OL/DA: %i/%i | Jumpoff Angle: %.3f | Airpath: %.4f]\n"...\ + "[Strafes: %i | Airtime: %i | Jump Direction: %s | %s%sHeight: %.4f%s%s%s%s]", + pd.failedJump ? "FAILED " : "", + g_jumpTypes[pd.jumpType], + pd.jumpDistance, + block, + hasBlock ? " | " : "", + edge, + hasEdge ? " | " : "", + pd.jumpVeer, + fwdRelease, + pd.jumpSync, + pd.jumpMaxspeed, + + landEdge, + hasLandEdge ? " | " : "", + pd.jumpPrespeed, + pd.jumpOverlap, + pd.jumpDeadair, + pd.jumpJumpoffAngle, + pd.jumpAirpath, + + pd.strafeCount + 1, + pd.jumpAirtime, + g_jumpDirString[pd.jumpDir], + fog, + hasFOG ? " | " : "", + pd.jumpHeight, + hasOffset ? " | " : "", + offset, + hasStamina ? " | " : "", + stamina + ); + + CRemoveTags(consoleStats, sizeof(consoleStats)); + ClientAndSpecsPrintConsole(client, consoleStats); + + if (!IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_STATS)) + { + ClientAndSpecsPrintConsole(client, " #. Sync Gain Loss Max Air OL DA AvgGain Avg efficiency, (max efficiency)"); + for (int strafe; strafe <= pd.strafeCount && strafe < MAX_STRAFES; strafe++) + { + ClientAndSpecsPrintConsole(client, "%2i. %5.1f%% %6.2f %6.2f %5.1f %3i %3i %3i %3.2f %3i%% (%3i%%)", + strafe + 1, + pd.strafeSync[strafe], + pd.strafeGain[strafe], + pd.strafeLoss[strafe], + pd.strafeMax[strafe], + pd.strafeAirtime[strafe], + pd.strafeOverlap[strafe], + pd.strafeDeadair[strafe], + pd.strafeAvgGain[strafe], + RoundFloat(pd.strafeAvgEfficiency[strafe]), + RoundFloat(pd.strafeMaxEfficiency[strafe]) + ); + } + } + + // hud text + char strafeLeft[512] = ""; + int slIndex; + char strafeRight[512] = ""; + int srIndex; + char mouseLeft[512] = ""; + int mlIndex; + char mouseRight[512] = ""; + int mrIndex; + + char hudStrafeLeft[4096] = ""; + int hslIndex; + char hudStrafeRight[4096] = ""; + int hsrIndex; + char hudMouse[4096] = ""; + int hmIndex; + + char mouseChars[][] = { + "▄", + "█" + }; + char mouseColours[][] = { + "|", + "|", + "|" + }; + float mouseSpeedScale = 1.0 / (512.0 * GetTickInterval()); + // nonsensical default values, so that the first comparison check fails + StrafeType lastStrafeTypeLeft = STRAFETYPE_NONE_RIGHT + STRAFETYPE_NONE_RIGHT; + StrafeType lastStrafeTypeRight = STRAFETYPE_NONE_RIGHT + STRAFETYPE_NONE_RIGHT; + int lastMouseIndex = 9999; + for (int i = 0; i < pd.jumpAirtime && i < MAX_JUMP_FRAMES; i++) + { + StrafeType strafeTypeLeft = pd.strafeGraph[i]; + StrafeType strafeTypeRight = pd.strafeGraph[i]; + + if (strafeTypeLeft == STRAFETYPE_RIGHT + || strafeTypeLeft == STRAFETYPE_NONE_RIGHT + || strafeTypeLeft == STRAFETYPE_OVERLAP_RIGHT) + { + strafeTypeLeft = STRAFETYPE_NONE; + } + + if (strafeTypeRight == STRAFETYPE_LEFT + || strafeTypeRight == STRAFETYPE_NONE_LEFT + || strafeTypeRight == STRAFETYPE_OVERLAP_LEFT) + { + strafeTypeRight = STRAFETYPE_NONE; + } + + slIndex += strcopy(strafeLeft[slIndex], sizeof(strafeLeft) - slIndex, g_szStrafeType[strafeTypeLeft]); + srIndex += strcopy(strafeRight[srIndex], sizeof(strafeRight) - srIndex, g_szStrafeType[strafeTypeRight]); + + int charIndex = GCIntMin(RoundToFloor(FloatAbs(pd.mouseGraph[i]) * mouseSpeedScale), 1); + if (pd.mouseGraph[i] == 0.0) + { + mouseLeft[mlIndex++] = '.'; + mouseRight[mrIndex++] = '.'; + } + else if (pd.mouseGraph[i] < 0.0) + { + mouseLeft[mlIndex++] = '.'; + mrIndex += strcopy(mouseRight[mrIndex], sizeof(mouseRight) - mrIndex, mouseChars[charIndex]); + } + else if (pd.mouseGraph[i] > 0.0) + { + mlIndex += strcopy(mouseLeft[mlIndex], sizeof(mouseLeft) - mlIndex, mouseChars[charIndex]); + mouseRight[mrIndex++] = '.'; + } + + if (i == 0) + { + hslIndex += strcopy(hudStrafeLeft, sizeof(hudStrafeLeft), "L: "); + hsrIndex += strcopy(hudStrafeRight, sizeof(hudStrafeRight), "R: "); + hmIndex += strcopy(hudMouse, sizeof(hudMouse), "M: "); + } + + if (lastStrafeTypeLeft != strafeTypeLeft) + { + hslIndex += strcopy(hudStrafeLeft[hslIndex], sizeof(hudStrafeLeft) - hslIndex, g_szStrafeTypeColour[strafeTypeLeft]); + } + else + { + hudStrafeLeft[hslIndex++] = '|'; + } + + if (lastStrafeTypeRight != strafeTypeRight) + { + hsrIndex += strcopy(hudStrafeRight[hsrIndex], sizeof(hudStrafeRight) - hsrIndex, g_szStrafeTypeColour[strafeTypeRight]); + } + else + { + hudStrafeRight[hsrIndex++] = '|'; + } + + int mouseIndex = FloatSign(pd.mouseGraph[i]) + 1; + if (mouseIndex != lastMouseIndex) + { + hmIndex += strcopy(hudMouse[hmIndex], sizeof(hudMouse) - hmIndex, mouseColours[mouseIndex]); + } + else + { + hudMouse[hmIndex++] = '|'; + } + + lastStrafeTypeLeft = strafeTypeLeft; + lastStrafeTypeRight = strafeTypeRight; + lastMouseIndex = mouseIndex; + } + + mouseLeft[mlIndex] = '\0'; + mouseRight[mrIndex] = '\0'; + hudStrafeLeft[hslIndex] = '\0'; + hudStrafeRight[hsrIndex] = '\0'; + hudMouse[hmIndex] = '\0'; + + bool showHudGraph = IsSettingEnabled(client, SETTINGS_SHOW_HUD_GRAPH); + if (showHudGraph) + { + // worst case scenario is roughly 11000 characters :D + char strafeGraph[11000]; + FormatEx(strafeGraph, sizeof(strafeGraph), "%s
%s
%s", hudStrafeLeft, hudStrafeRight, hudMouse); + + // TODO: sometimes just after a previous panel has faded out a new panel can't be shown, fix! + ShowPanel(client, 3, strafeGraph); + } + if (!IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_GRAPH)) + { + ClientAndSpecsPrintConsole(client, "\nStrafe keys:\nL: %s\nR: %s", strafeLeft, strafeRight); + ClientAndSpecsPrintConsole(client, "Mouse movement:\nL: %s\nR: %s\n\n", mouseLeft, mouseRight); + } +} diff --git a/sourcemod/scripting/distbugfix/clientprefs.sp b/sourcemod/scripting/distbugfix/clientprefs.sp new file mode 100644 index 0000000..bee4681 --- /dev/null +++ b/sourcemod/scripting/distbugfix/clientprefs.sp @@ -0,0 +1,51 @@ + + +static Handle distbugCookie; +static int settings[MAXPLAYERS + 1]; + +void OnPluginStart_Clientprefs() +{ + distbugCookie = RegClientCookie("distbugfix_cookie_v2", "cookie for distbugfix", CookieAccess_Private); + if (distbugCookie == INVALID_HANDLE) + { + SetFailState("Couldn't create distbug cookie."); + } +} + +void OnClientCookiesCached_Clientprefs(int client) +{ + char buffer[MAX_COOKIE_SIZE]; + GetClientCookie(client, distbugCookie, buffer, sizeof(buffer)); + + settings[client] = StringToInt(buffer); +} + +void SaveClientCookies(int client) +{ + if (!GCIsValidClient(client) || !AreClientCookiesCached(client)) + { + return; + } + + char buffer[MAX_COOKIE_SIZE]; + IntToString(settings[client], buffer, sizeof(buffer)); + SetClientCookie(client, distbugCookie, buffer); +} + +bool IsSettingEnabled(int client, int setting) +{ + if (GCIsValidClient(client)) + { + return !!(settings[client] & setting); + } + return false; +} + +void ToggleSetting(int client, int setting) +{ + if (GCIsValidClient(client)) + { + settings[client] ^= setting; + SaveClientCookies(client); + } +} \ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat.sp b/sourcemod/scripting/gokz-anticheat.sp new file mode 100644 index 0000000..9925eca --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat.sp @@ -0,0 +1,318 @@ +#include + +#include + +#include +#include +#include + +#include + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include +#include +#include + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Anti-Cheat", + author = "DanZay", + description = "Detects basic player movement cheats", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-anticheat.txt" + +bool gB_GOKZLocalDB; +bool gB_SourceBansPP; +bool gB_SourceBans; +bool gB_GOKZGlobal; + +Handle gH_DHooks_OnTeleport; + +int gI_CmdNum[MAXPLAYERS + 1]; +int gI_LastOriginTeleportCmdNum[MAXPLAYERS + 1]; + +int gI_ButtonCount[MAXPLAYERS + 1]; +int gI_ButtonsIndex[MAXPLAYERS + 1]; +int gI_Buttons[MAXPLAYERS + 1][AC_MAX_BUTTON_SAMPLES]; + +int gI_BhopCount[MAXPLAYERS + 1]; +int gI_BhopIndex[MAXPLAYERS + 1]; +int gI_BhopLastTakeoffCmdnum[MAXPLAYERS + 1]; +int gI_BhopLastRecordedBhopCmdnum[MAXPLAYERS + 1]; +bool gB_BhopHitPerf[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES]; +int gI_BhopPreJumpInputs[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES]; +int gI_BhopPostJumpInputs[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES]; +bool gB_BhopPostJumpInputsPending[MAXPLAYERS + 1]; +bool gB_LastLandingWasValid[MAXPLAYERS + 1]; +bool gB_BindExceptionPending[MAXPLAYERS + 1]; +bool gB_BindExceptionPostPending[MAXPLAYERS + 1]; + +ConVar gCV_gokz_autoban; +ConVar gCV_gokz_autoban_duration_bhop_hack; +ConVar gCV_gokz_autoban_duration_bhop_macro; +ConVar gCV_sv_autobunnyhopping; + +#include "gokz-anticheat/api.sp" +#include "gokz-anticheat/bhop_tracking.sp" +#include "gokz-anticheat/commands.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-anticheat"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-anticheat.phrases"); + + CreateConVars(); + CreateGlobalForwards(); + HookEvents(); + RegisterCommands(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = LibraryExists("gokz-localdb"); + gB_SourceBansPP = LibraryExists("sourcebans++"); + gB_SourceBans = LibraryExists("sourcebans"); + gB_GOKZGlobal = LibraryExists("gokz-global"); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_GOKZLocalDB = gB_GOKZLocalDB || StrEqual(name, "gokz-localdb"); + gB_SourceBansPP = gB_SourceBansPP || StrEqual(name, "sourcebans++"); + gB_SourceBans = gB_SourceBans || StrEqual(name, "sourcebans"); + gB_GOKZGlobal = gB_GOKZGlobal || StrEqual(name, "gokz-global"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb"); + gB_SourceBansPP = gB_SourceBansPP && !StrEqual(name, "sourcebans++"); + gB_SourceBans = gB_SourceBans && !StrEqual(name, "sourcebans"); + gB_GOKZGlobal = gB_GOKZGlobal && !StrEqual(name, "gokz-global"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + OnClientPutInServer_BhopTracking(client); + HookClientEvents(client); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + gI_CmdNum[client] = cmdnum; + return Plugin_Continue; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsPlayerAlive(client) || IsFakeClient(client)) + { + return; + } + + OnPlayerRunCmdPost_BhopTracking(client, buttons, cmdnum); +} + +public MRESReturn DHooks_OnTeleport(int client, Handle params) +{ + // Parameter 1 not null means origin affected + gI_LastOriginTeleportCmdNum[client] = !DHookIsNullParam(params, 1) ? gI_CmdNum[client] : gI_LastOriginTeleportCmdNum[client]; + + // Parameter 3 not null means velocity affected + //gI_LastVelocityTeleportCmdNum[client] = !DHookIsNullParam(params, 3) ? gI_CmdNum[client] : gI_LastVelocityTeleportCmdNum[client]; + + return MRES_Ignored; +} + +public void GOKZ_OnFirstSpawn(int client) +{ + GOKZ_PrintToChat(client, false, "%t", "Anti-Cheat Warning"); +} + +public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats) +{ + LogSuspicion(client, reason, notes, stats); +} + + + +// =====[ PUBLIC ]===== + +void SuspectPlayer(int client, ACReason reason, const char[] notes, const char[] stats) +{ + Call_OnPlayerSuspected(client, reason, notes, stats); + + if (gB_GOKZLocalDB) + { + GOKZ_DB_SetCheater(client, true); + } + + if (gCV_gokz_autoban.BoolValue) + { + BanSuspect(client, reason); + } +} + + + +// =====[ PRIVATE ]===== + +static void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-anticheat", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_autoban = AutoExecConfig_CreateConVar( + "gokz_autoban", + "1", + "Whether to autoban players when they are suspected of cheating.", + _, + true, + 0.0, + true, + 1.0); + + gCV_gokz_autoban_duration_bhop_hack = AutoExecConfig_CreateConVar( + "gokz_autoban_duration_bhop_hack", + "0", + "Duration of anticheat autobans for bunnyhop hacking in minutes (0 for permanent).", + _, + true, + 0.0); + + gCV_gokz_autoban_duration_bhop_macro = AutoExecConfig_CreateConVar( + "gokz_autoban_duration_bhop_macro", + "43200", // 30 days + "Duration of anticheat autobans for bunnyhop macroing in minutes (0 for permanent).", + _, + true, + 0.0); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + gCV_sv_autobunnyhopping = FindConVar("sv_autobunnyhopping"); +} + +static void HookEvents() +{ + GameData gameData = new GameData("sdktools.games"); + int offset; + + // Setup DHooks OnTeleport for players + offset = gameData.GetOffset("Teleport"); + gH_DHooks_OnTeleport = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnTeleport); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_ObjectPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_Bool); + + delete gameData; +} + +static void HookClientEvents(int client) +{ + DHookEntity(gH_DHooks_OnTeleport, true, client); +} + +static void LogSuspicion(int client, ACReason reason, const char[] notes, const char[] stats) +{ + char logPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, logPath, sizeof(logPath), AC_LOG_PATH); + + switch (reason) + { + case ACReason_BhopHack:LogToFileEx(logPath, "%L was suspected of bhop hacking. Notes - %s, Stats - %s", client, notes, stats); + case ACReason_BhopMacro:LogToFileEx(logPath, "%L was suspected of bhop macroing. Notes - %s, Stats - %s", client, notes, stats); + } +} + +static void BanSuspect(int client, ACReason reason) +{ + char banMessage[128]; + char redirectString[64] = "Contact the server administrator for more info"; + + if (gB_GOKZGlobal) + { + redirectString = "Visit http://rules.global-api.com/ for more info"; + } + + switch (reason) + { + case ACReason_BhopHack: + { + FormatEx(banMessage, sizeof(banMessage), "You have been banned for using a %s.\n%s", "bhop hack", redirectString); + AutoBanClient( + client, + gCV_gokz_autoban_duration_bhop_hack.IntValue, + "gokz-anticheat - Bhop hacking", + banMessage); + } + case ACReason_BhopMacro: + { + FormatEx(banMessage, sizeof(banMessage), "You have been banned for using a %s.\n%s", "bhop macro", redirectString); + AutoBanClient( + client, + gCV_gokz_autoban_duration_bhop_macro.IntValue, + "gokz-anticheat - Bhop macroing", + banMessage); + } + } +} + +static void AutoBanClient(int client, int minutes, const char[] reason, const char[] kickMessage) +{ + if (gB_SourceBansPP) + { + SBPP_BanPlayer(0, client, minutes, reason); + } + else if (gB_SourceBans) + { + SBBanPlayer(0, client, minutes, reason); + } + else + { + BanClient(client, minutes, BANFLAG_AUTO, reason, kickMessage, "gokz-anticheat", 0); + } +} \ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/api.sp b/sourcemod/scripting/gokz-anticheat/api.sp new file mode 100644 index 0000000..7f99724 --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/api.sp @@ -0,0 +1,174 @@ +static GlobalForward H_OnPlayerSuspected; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnPlayerSuspected = new GlobalForward("GOKZ_AC_OnPlayerSuspected", ET_Ignore, Param_Cell, Param_Cell, Param_String, Param_String); +} + +void Call_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats) +{ + Call_StartForward(H_OnPlayerSuspected); + Call_PushCell(client); + Call_PushCell(reason); + Call_PushString(notes); + Call_PushString(stats); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_AC_GetSampleSize", Native_GetSampleSize); + CreateNative("GOKZ_AC_GetHitPerf", Native_GetHitPerf); + CreateNative("GOKZ_AC_GetPerfCount", Native_GetPerfCount); + CreateNative("GOKZ_AC_GetPerfRatio", Native_GetPerfRatio); + CreateNative("GOKZ_AC_GetJumpInputs", Native_GetJumpInputs); + CreateNative("GOKZ_AC_GetAverageJumpInputs", Native_GetAverageJumpInputs); + CreateNative("GOKZ_AC_GetPreJumpInputs", Native_GetPreJumpInputs); + CreateNative("GOKZ_AC_GetPostJumpInputs", Native_GetPostJumpInputs); +} + +public int Native_GetSampleSize(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return IntMin(gI_BhopCount[client], AC_MAX_BHOP_SAMPLES); +} + +public int Native_GetHitPerf(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + bool[] perfs = new bool[sampleSize]; + SortByRecent(gB_BhopHitPerf[client], AC_MAX_BHOP_SAMPLES, perfs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, perfs, sampleSize); + return sampleSize; +} + +public int Native_GetPerfCount(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return 0; + } + + bool[] perfs = new bool[sampleSize]; + GOKZ_AC_GetHitPerf(client, perfs, sampleSize); + + int perfCount = 0; + for (int i = 0; i < sampleSize; i++) + { + if (perfs[i]) + { + perfCount++; + } + } + return perfCount; +} + +public int Native_GetPerfRatio(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return view_as(0.0); + } + + int perfCount = GOKZ_AC_GetPerfCount(client, sampleSize); + return view_as(float(perfCount) / float(sampleSize)); +} + +public int Native_GetJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] preJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]); + int[] postJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]); + + int[] jumpInputs = new int[sampleSize]; + for (int i = 0; i < sampleSize; i++) + { + jumpInputs[i] = preJumpInputs[i] + postJumpInputs[i]; + } + + SetNativeArray(2, jumpInputs, sampleSize); + return sampleSize; +} + +public int Native_GetAverageJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return view_as(0.0); + } + + int[] jumpInputs = new int[sampleSize]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, sampleSize); + + int jumpInputCount = 0; + for (int i = 0; i < sampleSize; i++) + { + jumpInputCount += jumpInputs[i]; + } + return view_as(float(jumpInputCount) / float(sampleSize)); +} + +public int Native_GetPreJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] preJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, preJumpInputs, sampleSize); + return sampleSize; +} + +public int Native_GetPostJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] postJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, postJumpInputs, sampleSize); + return sampleSize; +} \ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp new file mode 100644 index 0000000..5607b07 --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp @@ -0,0 +1,336 @@ +/* + Track player's jump inputs and whether they hit perfect + bunnyhops for a number of their recent bunnyhops. +*/ + + + +// =====[ PUBLIC ]===== + +void PrintBhopCheckToChat(int client, int target) +{ + GOKZ_PrintToChat(client, true, + "{lime}%N {grey}[{lime}%d%%%% {grey}%t | {lime}%.2f {grey}%t]", + target, + RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0), + "Perfs", + GOKZ_AC_GetAverageJumpInputs(target, 20), + "Average"); + GOKZ_PrintToChat(client, false, + " {grey}%t - %s", + "Pattern", + GenerateScrollPattern(target, 20)); +} + +void PrintBhopCheckToConsole(int client, int target) +{ + PrintToConsole(client, + "%N [%d%% %t | %.2f %t]\n %t - %s", + target, + RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0), + "Perfs", + GOKZ_AC_GetAverageJumpInputs(target, 20), + "Average", + "Pattern", + GenerateScrollPattern(target, 20, false)); +} + +// Generate 'scroll pattern' +char[] GenerateScrollPattern(int client, int sampleSize = AC_MAX_BHOP_SAMPLES, bool colours = true) +{ + char report[512]; + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + bool[] perfs = new bool[maxIndex]; + GOKZ_AC_GetHitPerf(client, perfs, maxIndex); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + + for (int i = 0; i < maxIndex; i++) + { + if (colours) + { + Format(report, sizeof(report), "%s%s%d ", + report, + perfs[i] ? "{green}" : "{default}", + jumpInputs[i]); + } + else + { + Format(report, sizeof(report), "%s%d%s ", + report, + jumpInputs[i], + perfs[i] ? "*" : ""); + } + } + + TrimString(report); + + return report; +} + +// Generate 'scroll pattern' report showing pre and post inputs instead +char[] GenerateScrollPatternEx(int client, int sampleSize = AC_MAX_BHOP_SAMPLES) +{ + char report[512]; + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + bool[] perfs = new bool[maxIndex]; + GOKZ_AC_GetHitPerf(client, perfs, maxIndex); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + int[] preJumpInputs = new int[maxIndex]; + GOKZ_AC_GetPreJumpInputs(client, preJumpInputs, maxIndex); + int[] postJumpInputs = new int[maxIndex]; + GOKZ_AC_GetPostJumpInputs(client, postJumpInputs, maxIndex); + + for (int i = 0; i < maxIndex; i++) + { + Format(report, sizeof(report), "%s(%d%s%d)", + report, + preJumpInputs[i], + perfs[i] ? "*" : " ", + postJumpInputs[i]); + } + + TrimString(report); + + return report; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_BhopTracking(int client) +{ + ResetBhopStats(client); +} + +void OnPlayerRunCmdPost_BhopTracking(int client, int buttons, int cmdnum) +{ + if (gCV_sv_autobunnyhopping.BoolValue) + { + return; + } + + int nextIndex = NextIndex(gI_BhopIndex[client], AC_MAX_BHOP_SAMPLES); + + // Record buttons BEFORE checking for bhop + RecordButtons(client, buttons); + + // If bhop was last tick, then record the pre bhop inputs. + // Require two times the button sample size since the last + // takeoff to avoid pre and post bhop input overlap. + if (HitBhop(client, cmdnum) + && cmdnum >= gI_BhopLastTakeoffCmdnum[client] + AC_MAX_BUTTON_SAMPLES * 2 + && gB_LastLandingWasValid[client]) + { + gB_BhopHitPerf[client][nextIndex] = Movement_GetHitPerf(client); + gI_BhopPreJumpInputs[client][nextIndex] = CountJumpInputs(client); + gI_BhopLastRecordedBhopCmdnum[client] = cmdnum; + gB_BhopPostJumpInputsPending[client] = true; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = false; + } + + // Bind exception + if (gB_BindExceptionPending[client] && cmdnum > Movement_GetLandingCmdNum(client) + AC_MAX_BHOP_GROUND_TICKS) + { + gB_BhopHitPerf[client][nextIndex] = false; + gI_BhopPreJumpInputs[client][nextIndex] = -1; // Special value for binded jumps + gI_BhopLastRecordedBhopCmdnum[client] = cmdnum; + gB_BhopPostJumpInputsPending[client] = true; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = true; + } + + // Record post bhop inputs once enough ticks have passed + if (gB_BhopPostJumpInputsPending[client] && cmdnum == gI_BhopLastRecordedBhopCmdnum[client] + AC_MAX_BUTTON_SAMPLES) + { + gI_BhopPostJumpInputs[client][nextIndex] = CountJumpInputs(client); + gB_BhopPostJumpInputsPending[client] = false; + gI_BhopIndex[client] = nextIndex; + gI_BhopCount[client]++; + CheckForBhopMacro(client); + gB_BindExceptionPostPending[client] = false; + } + + // Record last jump takeoff time + if (JustJumped(client, cmdnum)) + { + gI_BhopLastTakeoffCmdnum[client] = cmdnum; + gB_BindExceptionPending[client] = false; + if (gB_BindExceptionPostPending[client]) + { + gB_BhopPostJumpInputsPending[client] = false; + gB_BindExceptionPostPending[client] = false; + } + } + + if (JustLanded(client, cmdnum)) + { + // These conditions exist to reduce false positives. + + // Telehopping is when the player bunnyhops out of a teleport that has a + // destination very close to the ground. This will, more than usual, + // result in a perfect bunnyhop. This is alleviated by checking if the + // player's origin was affected by a teleport last tick. + + // When a player is pressing up against a slope but not ascending it (e.g. + // palm trees on kz_adv_cursedjourney), they will switch between on ground + // and off ground frequently, which means that if they manage to jump, the + // jump will be recorded as a perfect bunnyhop. To ignore this, we check + // the jump is more than 1 tick duration. + + gB_LastLandingWasValid[client] = cmdnum - gI_LastOriginTeleportCmdNum[client] > 1 + && cmdnum - Movement_GetTakeoffCmdNum(client) > 1; + + // You can still crouch-bind VNL jumps and some people just don't know that + // it doesn't work with the other modes in GOKZ. This can cause false positives + // if the player uses the bind for bhops and mostly presses it too early or + // exactly on time rather than too late. This is supposed to reduce those by + // detecting jumps where you don't get a bhop and have exactly one jump input + // before landing and none after landing. We require the one input to be right + // before the jump to make it a lot harder to fake a binded jump when doing + // a regular longjump. + gB_BindExceptionPending[client] = (CountJumpInputs(client, AC_BINDEXCEPTION_SAMPLES) == 1 && CountJumpInputs(client, AC_MAX_BUTTON_SAMPLES) == 1); + gB_BindExceptionPostPending[client] = false; + } +} + + + +// =====[ PRIVATE ]===== + +static void CheckForBhopMacro(int client) +{ + if (GOKZ_AC_GetPerfCount(client, 19) == 19) + { + SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 19)); + } + else if (GOKZ_AC_GetPerfCount(client, 30) >= 28) + { + SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 30)); + } + else if (GOKZ_AC_GetPerfCount(client, 20) >= 16 && GOKZ_AC_GetAverageJumpInputs(client, 20) <= 2.0 + EPSILON) + { + SuspectPlayer(client, ACReason_BhopHack, "1's or 2's scroll pattern", GenerateBhopBanStats(client, 20)); + } + else if (gI_BhopCount[client] >= 20 && GOKZ_AC_GetPerfCount(client, 20) >= 8 + && GOKZ_AC_GetAverageJumpInputs(client, 20) >= 19.0 - EPSILON) + { + SuspectPlayer(client, ACReason_BhopMacro, "High scroll pattern", GenerateBhopBanStats(client, 20)); + } + else if (GOKZ_AC_GetPerfCount(client, 30) >= 10 && CheckForRepeatingJumpInputsCount(client, 25, 30) >= 14) + { + SuspectPlayer(client, ACReason_BhopMacro, "Repeating scroll pattern", GenerateBhopBanStats(client, 30)); + } +} + +static char[] GenerateBhopBanStats(int client, int sampleSize) +{ + char stats[512]; + FormatEx(stats, sizeof(stats), + "Perfs: %d/%d, Average: %.2f, Scroll pattern: %s", + GOKZ_AC_GetPerfCount(client, sampleSize), + IntMin(gI_BhopCount[client], sampleSize), + GOKZ_AC_GetAverageJumpInputs(client, sampleSize), + GenerateScrollPatternEx(client, sampleSize)); + return stats; +} + +/** + * Returns -1, or the repeating input count if there if there is + * an input count that repeats for more than the provided ratio. + * + * @param client Client index. + * @param threshold Minimum frequency to be considered 'repeating'. + * @param sampleSize Maximum recent bhop samples to include in calculation. + * @return The repeating input, or else -1. + */ +static int CheckForRepeatingJumpInputsCount(int client, int threshold, int sampleSize = AC_MAX_BHOP_SAMPLES) +{ + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + int maxJumpInputs = AC_MAX_BUTTON_SAMPLES + 1; + int[] jumpInputsFrequency = new int[maxJumpInputs]; + + // Count up all the in jump patterns + for (int i = 0; i < maxIndex; i++) + { + // -1 is a binded jump, those are excluded + if (jumpInputs[i] != -1) + { + jumpInputsFrequency[jumpInputs[i]]++; + } + } + + // Returns i if the given number of the sample size has the same jump input count + for (int i = 1; i < maxJumpInputs; i++) + { + if (jumpInputsFrequency[i] >= threshold) + { + return i; + } + } + + return -1; // -1 if no repeating jump input found +} + +// Reset the tracked bhop stats of the client +static void ResetBhopStats(int client) +{ + gI_ButtonCount[client] = 0; + gI_ButtonsIndex[client] = 0; + gI_BhopCount[client] = 0; + gI_BhopIndex[client] = 0; + gI_BhopLastTakeoffCmdnum[client] = 0; + gI_BhopLastRecordedBhopCmdnum[client] = 0; + gB_BhopPostJumpInputsPending[client] = false; + gB_LastLandingWasValid[client] = false; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = false; +} + +// Returns true if ther was a jump last tick and was within a number of ticks after landing +static bool HitBhop(int client, int cmdnum) +{ + return JustJumped(client, cmdnum) && Movement_GetTakeoffCmdNum(client) - Movement_GetLandingCmdNum(client) <= AC_MAX_BHOP_GROUND_TICKS; +} + +static bool JustJumped(int client, int cmdnum) +{ + return Movement_GetJumped(client) && Movement_GetTakeoffCmdNum(client) == cmdnum; +} + +static bool JustLanded(int client, int cmdnum) +{ + return Movement_GetLandingCmdNum(client) == cmdnum; +} + +// Records current button inputs +static void RecordButtons(int client, int buttons) +{ + gI_ButtonsIndex[client] = NextIndex(gI_ButtonsIndex[client], AC_MAX_BUTTON_SAMPLES); + gI_Buttons[client][gI_ButtonsIndex[client]] = buttons; + gI_ButtonCount[client]++; +} + +// Counts the number of times buttons went from !IN_JUMP to IN_JUMP +static int CountJumpInputs(int client, int sampleSize = AC_MAX_BUTTON_SAMPLES) +{ + int[] recentButtons = new int[sampleSize]; + SortByRecent(gI_Buttons[client], AC_MAX_BUTTON_SAMPLES, recentButtons, sampleSize, gI_ButtonsIndex[client]); + int maxIndex = IntMin(gI_ButtonCount[client], sampleSize); + int jumps = 0; + + for (int i = 0; i < maxIndex - 1; i++) + { + // If buttons went from !IN_JUMP to IN_JUMP + if (!(recentButtons[i + 1] & IN_JUMP) && recentButtons[i] & IN_JUMP) + { + jumps++; + } + } + return jumps; +} \ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/commands.sp b/sourcemod/scripting/gokz-anticheat/commands.sp new file mode 100644 index 0000000..a1fbe2e --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/commands.sp @@ -0,0 +1,76 @@ +void RegisterCommands() +{ + RegAdminCmd("sm_bhopcheck", CommandBhopCheck, ADMFLAG_ROOT, "[KZ] Show bunnyhop stats report including perf ratio and scroll pattern."); +} + +public Action CommandBhopCheck(int client, int args) +{ + if (args == 0) + { + if (GOKZ_AC_GetSampleSize(client) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)"); + } + else + { + PrintBhopCheckToChat(client, client); + } + return Plugin_Handled; + } + + char arg[65]; + GetCmdArg(1, arg, sizeof(arg)); + char targetName[MAX_TARGET_LENGTH]; + int targetList[MAXPLAYERS], targetCount; + bool tnIsML; + + if ((targetCount = ProcessTargetString( + arg, + client, + targetList, + MAXPLAYERS, + COMMAND_FILTER_NO_IMMUNITY | COMMAND_FILTER_NO_BOTS, + targetName, + sizeof(targetName), + tnIsML)) <= 0) + { + ReplyToTargetError(client, targetCount); + return Plugin_Handled; + } + + if (targetCount >= 2) + { + GOKZ_PrintToChat(client, true, "%t", "See Console"); + for (int i = 0; i < targetCount; i++) + { + if (GOKZ_AC_GetSampleSize(targetList[i]) == 0) + { + PrintToConsole(client, "%t", "Not Enough Bhops (Console)", targetList[i]); + } + else + { + PrintBhopCheckToConsole(client, targetList[i]); + } + } + } + else + { + if (GOKZ_AC_GetSampleSize(targetList[0]) == 0) + { + if (targetList[0] == client) + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops", targetList[0]); + } + } + else + { + PrintBhopCheckToChat(client, targetList[0]); + } + } + + return Plugin_Handled; +} \ No newline at end of file diff --git a/sourcemod/scripting/gokz-chat.sp b/sourcemod/scripting/gokz-chat.sp new file mode 100644 index 0000000..38820f8 --- /dev/null +++ b/sourcemod/scripting/gokz-chat.sp @@ -0,0 +1,309 @@ +#include + +#include + +#include + +#include +#include + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include +#include + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Chat", + author = "DanZay", + description = "Handles client-triggered chat messages", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-chat.txt" + +bool gB_BaseComm; +char gC_PlayerTags[MAXPLAYERS + 1][32]; +char gC_PlayerTagColors[MAXPLAYERS + 1][16]; + +ConVar gCV_gokz_chat_processing; +ConVar gCV_gokz_connection_messages; + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + CreateNatives(); + RegPluginLibrary("gokz-chat"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("gokz-chat.phrases"); + + CreateConVars(); + HookEvents(); + + OnPluginStart_BlockRadio(); + OnPluginStart_BlockChatWheel(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_BaseComm = LibraryExists("basecomm"); +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + gB_BaseComm = gB_BaseComm || StrEqual(name, "basecomm"); +} + +public void OnLibraryRemoved(const char[] name) +{ + gB_BaseComm = gB_BaseComm && !StrEqual(name, "basecomm"); +} + + + +// =====[ CLIENT EVENTS ]===== + +public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs) +{ + if (client > 0 && gCV_gokz_chat_processing.BoolValue && IsClientInGame(client)) + { + OnClientSayCommand_ChatProcessing(client, command, sArgs); + return Plugin_Handled; + } + return Plugin_Continue; +} + +public void OnClientConnected(int client) +{ + gC_PlayerTags[client][0] = '\0'; + gC_PlayerTagColors[client][0] = '\0'; +} + +public void OnClientPutInServer(int client) +{ + PrintConnectMessage(client); +} + +public Action OnPlayerDisconnect(Event event, const char[] name, bool dontBroadcast) // player_disconnect pre hook +{ + event.BroadcastDisabled = true; // Block disconnection messages + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + PrintDisconnectMessage(client, event); + } + return Plugin_Continue; +} + +public Action OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast) // player_team pre hook +{ + event.SetBool("silent", true); // Block join team messages + return Plugin_Continue; +} + + + +// =====[ GENERAL ]===== + +void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-chat", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_chat_processing = AutoExecConfig_CreateConVar("gokz_chat_processing", "1", "Whether GOKZ processes player chat messages.", _, true, 0.0, true, 1.0); + gCV_gokz_connection_messages = AutoExecConfig_CreateConVar("gokz_connection_messages", "1", "Whether GOKZ handles connection and disconnection messages.", _, true, 0.0, true, 1.0); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); +} + +void HookEvents() +{ + HookEvent("player_disconnect", OnPlayerDisconnect, EventHookMode_Pre); + HookEvent("player_team", OnPlayerJoinTeam, EventHookMode_Pre); +} + + + +// =====[ CHAT PROCESSING ]===== + +void OnClientSayCommand_ChatProcessing(int client, const char[] command, const char[] message) +{ + if (gB_BaseComm && BaseComm_IsClientGagged(client) + || UsedBaseChat(client, command, message)) + { + return; + } + + // Resend messages that may have been a command with capital letters + if ((message[0] == '!' || message[0] == '/') && IsCharUpper(message[1])) + { + char loweredMessage[128]; + String_ToLower(message, loweredMessage, sizeof(loweredMessage)); + FakeClientCommand(client, "say %s", loweredMessage); + return; + } + + char sanitisedMessage[128]; + strcopy(sanitisedMessage, sizeof(sanitisedMessage), message); + SanitiseChatInput(sanitisedMessage, sizeof(sanitisedMessage)); + + char sanitisedName[MAX_NAME_LENGTH]; + GetClientName(client, sanitisedName, sizeof(sanitisedName)); + SanitiseChatInput(sanitisedName, sizeof(sanitisedName)); + + if (TrimString(sanitisedMessage) == 0) + { + return; + } + + if (IsSpectating(client)) + { + GOKZ_PrintToChatAll(false, "{default}* %s%s{lime}%s{default} : %s", + gC_PlayerTagColors[client], gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToConsoleAll("* %s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToServer("* %s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + } + else + { + GOKZ_PrintToChatAll(false, "%s%s{lime}%s{default} : %s", + gC_PlayerTagColors[client], gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToConsoleAll("%s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + PrintToServer("%s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage); + } +} + +bool UsedBaseChat(int client, const char[] command, const char[] message) +{ + // Assuming base chat is in use, check if message will get processed by basechat + if (message[0] != '@') + { + return false; + } + + if (strcmp(command, "say_team", false) == 0) + { + return true; + } + else if (strcmp(command, "say", false) == 0 && CheckCommandAccess(client, "sm_say", ADMFLAG_CHAT)) + { + return true; + } + + return false; +} + +void SanitiseChatInput(char[] message, int maxlength) +{ + Color_StripFromChatText(message, message, maxlength); + //CRemoveColors(message, maxlength); + // Chat gets double formatted, so replace '%' with '%%%%' to end up with '%' + ReplaceString(message, maxlength, "%", "%%%%"); +} + + + +// =====[ CONNECTION MESSAGES ]===== + +void PrintConnectMessage(int client) +{ + if (!gCV_gokz_connection_messages.BoolValue || IsFakeClient(client)) + { + return; + } + + GOKZ_PrintToChatAll(false, "%t", "Client Connection Message", client); +} + +void PrintDisconnectMessage(int client, Event event) // Hooked to player_disconnect event +{ + if (!gCV_gokz_connection_messages.BoolValue || IsFakeClient(client)) + { + return; + } + + char reason[128]; + event.GetString("reason", reason, sizeof(reason)); + GOKZ_PrintToChatAll(false, "%t", "Client Disconnection Message", client, reason); +} + + + +// =====[ BLOCK RADIO AND CHATWHEEL]===== + +static char radioCommands[][] = +{ + "coverme", "takepoint", "holdpos", "regroup", "followme", "takingfire", "go", + "fallback", "sticktog", "getinpos", "stormfront", "report", "roger", "enemyspot", + "needbackup", "sectorclear", "inposition", "reportingin", "getout", "negative", + "enemydown", "compliment", "thanks", "cheer", "go_a", "go_b", "sorry", "needrop" +}; + +public void OnPluginStart_BlockRadio() +{ + for (int i = 0; i < sizeof(radioCommands); i++) + { + AddCommandListener(CommandBlock, radioCommands[i]); + } +} + +public void OnPluginStart_BlockChatWheel() +{ + AddCommandListener(CommandBlock, "playerchatwheel"); + AddCommandListener(CommandBlock, "chatwheel_ping"); +} + +public Action CommandBlock(int client, const char[] command, int argc) +{ + return Plugin_Handled; +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_CH_SetChatTag", Native_SetChatTag); +} + +public int Native_SetChatTag(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + + char str[64]; + GetNativeString(2, str, sizeof(str)); + if (str[0] == '\0') + { + // To prevent the space after the mode + FormatEx(gC_PlayerTags[client], sizeof(gC_PlayerTags[]), "[%s] ", gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]); + } + else + { + FormatEx(gC_PlayerTags[client], sizeof(gC_PlayerTags[]), "[%s %s] ", gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)], str); + } + + GetNativeString(3, gC_PlayerTagColors[client], sizeof(gC_PlayerTagColors[])); + return 0; +} diff --git a/sourcemod/scripting/gokz-core.sp b/sourcemod/scripting/gokz-core.sp new file mode 100644 index 0000000..897403f --- /dev/null +++ b/sourcemod/scripting/gokz-core.sp @@ -0,0 +1,543 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#undef REQUIRE_EXTENSIONS +#undef REQUIRE_PLUGIN +#include + +#include +#include + +#pragma newdecls required +#pragma semicolon 1 + + + +public Plugin myinfo = +{ + name = "GOKZ Core", + author = "DanZay", + description = "Core plugin of the GOKZ plugin set", + version = GOKZ_VERSION, + url = GOKZ_SOURCE_URL +}; + +#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-core.txt" + +Handle gH_ThisPlugin; +Handle gH_DHooks_OnTeleport; +Handle gH_DHooks_SetModel; + +int gI_CmdNum[MAXPLAYERS + 1]; +int gI_TickCount[MAXPLAYERS + 1]; +bool gB_OldOnGround[MAXPLAYERS + 1]; +int gI_OldButtons[MAXPLAYERS + 1]; +int gI_TeleportCmdNum[MAXPLAYERS + 1]; +bool gB_OriginTeleported[MAXPLAYERS + 1]; +bool gB_VelocityTeleported[MAXPLAYERS + 1]; +bool gB_LateLoad; + +ConVar gCV_gokz_chat_prefix; +ConVar gCV_sv_full_alltalk; + +#include "gokz-core/commands.sp" +#include "gokz-core/modes.sp" +#include "gokz-core/misc.sp" +#include "gokz-core/options.sp" +#include "gokz-core/teleports.sp" +#include "gokz-core/triggerfix.sp" +#include "gokz-core/demofix.sp" +#include "gokz-core/teamnumfix.sp" + +#include "gokz-core/map/buttons.sp" +#include "gokz-core/map/triggers.sp" +#include "gokz-core/map/mapfile.sp" +#include "gokz-core/map/prefix.sp" +#include "gokz-core/map/starts.sp" +#include "gokz-core/map/zones.sp" +#include "gokz-core/map/end.sp" + +#include "gokz-core/menus/mode_menu.sp" +#include "gokz-core/menus/options_menu.sp" + +#include "gokz-core/timer/pause.sp" +#include "gokz-core/timer/timer.sp" +#include "gokz-core/timer/virtual_buttons.sp" + +#include "gokz-core/forwards.sp" +#include "gokz-core/natives.sp" + + + +// =====[ PLUGIN EVENTS ]===== + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + if (GetEngineVersion() != Engine_CSGO) + { + SetFailState("GOKZ only supports CS:GO servers."); + } + + gH_ThisPlugin = myself; + gB_LateLoad = late; + + CreateNatives(); + RegPluginLibrary("gokz-core"); + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("gokz-common.phrases"); + LoadTranslations("gokz-core.phrases"); + + CreateGlobalForwards(); + CreateConVars(); + HookEvents(); + RegisterCommands(); + + OnPluginStart_MapTriggers(); + OnPluginStart_MapButtons(); + OnPluginStart_MapStarts(); + OnPluginStart_MapEnd(); + OnPluginStart_MapZones(); + OnPluginStart_Options(); + OnPluginStart_Triggerfix(); + OnPluginStart_Demofix(); + OnPluginStart_MapFile(); + OnPluginStart_TeamNumber(); +} + +public void OnAllPluginsLoaded() +{ + if (LibraryExists("updater")) + { + Updater_AddPlugin(UPDATER_URL); + } + + OnAllPluginsLoaded_Modes(); + OnAllPluginsLoaded_OptionsMenu(); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + OnClientPutInServer(client); + } + if (AreClientCookiesCached(client)) + { + OnClientCookiesCached(client); + } + } +} + +public void OnLibraryAdded(const char[] name) +{ + if (StrEqual(name, "updater")) + { + Updater_AddPlugin(UPDATER_URL); + } +} + + + +// =====[ CLIENT EVENTS ]===== + +public void OnClientPutInServer(int client) +{ + OnClientPutInServer_Timer(client); + OnClientPutInServer_Pause(client); + OnClientPutInServer_Teleports(client); + OnClientPutInServer_JoinTeam(client); + OnClientPutInServer_FirstSpawn(client); + OnClientPutInServer_VirtualButtons(client); + OnClientPutInServer_Options(client); + OnClientPutInServer_MapTriggers(client); + OnClientPutInServer_Triggerfix(client); + OnClientPutInServer_Noclip(client); + OnClientPutInServer_Turnbinds(client); + HookClientEvents(client); +} + +public void OnClientDisconnect(int client) +{ + OnClientDisconnect_Timer(client); + OnClientDisconnect_ValidJump(client); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) +{ + gI_CmdNum[client] = cmdnum; + gI_TickCount[client] = tickcount; + OnPlayerRunCmd_Triggerfix(client); + OnPlayerRunCmd_MapTriggers(client, buttons); + OnPlayerRunCmd_Turnbinds(client, buttons, tickcount, angles); + return Plugin_Continue; +} + +public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) +{ + if (!IsValidClient(client)) + { + return; + } + + OnPlayerRunCmdPost_VirtualButtons(client, buttons, cmdnum); // Emulate buttons first + OnPlayerRunCmdPost_Timer(client); // This should be first after emulating buttons + OnPlayerRunCmdPost_ValidJump(client); + UpdateTrackingVariables(client, cmdnum, buttons); // This should be last +} + +public void OnClientCookiesCached(int client) +{ + OnClientCookiesCached_Options(client); +} + +public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerSpawn_MapTriggers(client); + OnPlayerSpawn_Modes(client); + OnPlayerSpawn_Pause(client); + OnPlayerSpawn_ValidJump(client); + OnPlayerSpawn_FirstSpawn(client); + OnPlayerSpawn_GodMode(client); + OnPlayerSpawn_PlayerCollision(client); + } +} + +public Action OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerJoinTeam_TeamNumber(event, client); + } + return Plugin_Continue; +} + +public Action OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) // player_death pre hook +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if (IsValidClient(client)) + { + OnPlayerDeath_Timer(client); + OnPlayerDeath_ValidJump(client); + OnPlayerDeath_TeamNumber(client); + } + return Plugin_Continue; +} + +public void OnPlayerJump(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + OnPlayerJump_Triggers(client); +} + +public MRESReturn DHooks_OnTeleport(int client, Handle params) +{ + gB_OriginTeleported[client] = !DHookIsNullParam(params, 1); // Origin affected + gB_VelocityTeleported[client] = !DHookIsNullParam(params, 3); // Velocity affected + OnTeleport_ValidJump(client); + OnTeleport_DelayVirtualButtons(client); + return MRES_Ignored; +} + +public MRESReturn DHooks_OnSetModel(int client, Handle params) +{ + OnSetModel_PlayerCollision(client); + return MRES_Handled; +} + +public void OnCSPlayerSpawnPost(int client) +{ + if (GetEntPropEnt(client, Prop_Send, "m_hGroundEntity") == -1) + { + SetEntityFlags(client, GetEntityFlags(client) & ~FL_ONGROUND); + } +} + +public void OnClientPreThinkPost(int client) +{ + OnClientPreThinkPost_UseButtons(client); +} + +public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype) +{ + OnChangeMovetype_Timer(client, newMovetype); + OnChangeMovetype_Pause(client, newMovetype); + OnChangeMovetype_ValidJump(client, oldMovetype, newMovetype); + OnChangeMovetype_MapTriggers(client, newMovetype); +} + +public void Movement_OnStartTouchGround(int client) +{ + OnStartTouchGround_MapZones(client); + OnStartTouchGround_MapTriggers(client); +} + +public void Movement_OnStopTouchGround(int client, bool jumped, bool ladderJump, bool jumpbug) +{ + OnStopTouchGround_ValidJump(client, jumped, ladderJump, jumpbug); + OnStopTouchGround_MapTriggers(client); +} + +public void GOKZ_OnTimerStart_Post(int client, int course) +{ + OnTimerStart_JoinTeam(client); + OnTimerStart_Pause(client); + OnTimerStart_Teleports(client); +} + +public void GOKZ_OnTeleportToStart_Post(int client) +{ + OnTeleportToStart_Timer(client); +} + +public void GOKZ_OnCountedTeleport_Post(int client) +{ + OnCountedTeleport_VirtualButtons(client); +} + +public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue) +{ + Option coreOption; + if (!GOKZ_IsCoreOption(option, coreOption)) + { + return; + } + + OnOptionChanged_Options(client, coreOption, newValue); + OnOptionChanged_Timer(client, coreOption); + OnOptionChanged_Mode(client, coreOption); +} + +public void GOKZ_OnJoinTeam(int client, int team) +{ + OnJoinTeam_Pause(client, team); +} + + + +// =====[ OTHER EVENTS ]===== + +public void OnMapStart() +{ + OnMapStart_MapTriggers(); + OnMapStart_KZConfig(); + OnMapStart_Options(); + OnMapStart_Prefix(); + OnMapStart_CourseRegister(); + OnMapStart_MapStarts(); + OnMapStart_MapEnd(); + OnMapStart_VirtualButtons(); + OnMapStart_FixMissingSpawns(); + OnMapStart_Checkpoints(); + OnMapStart_TeamNumber(); + OnMapStart_Demofix(); +} + +public void OnMapEnd() +{ + OnMapEnd_Demofix(); +} + +public void OnGameFrame() +{ + OnGameFrame_TeamNumber(); + OnGameFrame_Triggerfix(); +} + +public void OnConfigsExecuted() +{ + OnConfigsExecuted_TimeLimit(); + OnConfigsExecuted_OptionsMenu(); +} + +public Action OnNormalSound(int clients[MAXPLAYERS], int &numClients, char sample[PLATFORM_MAX_PATH], int &entity, int &channel, float &volume, int &level, int &pitch, int &flags, char soundEntry[PLATFORM_MAX_PATH], int &seed) +{ + if (OnNormalSound_StopSounds(entity) == Plugin_Handled) + { + return Plugin_Handled; + } + return Plugin_Continue; +} + +public void OnEntityCreated(int entity, const char[] classname) +{ + // Don't react to player related entities + if (StrEqual(classname, "predicted_viewmodel") || StrEqual(classname, "item_assaultsuit") + || StrEqual(classname, "cs_bot") || StrEqual(classname, "player") + || StrContains(classname, "weapon") != -1) + { + return; + } + SDKHook(entity, SDKHook_Spawn, OnEntitySpawned); + SDKHook(entity, SDKHook_SpawnPost, OnEntitySpawnedPost); + OnEntityCreated_Triggerfix(entity, classname); +} + +public void OnEntitySpawned(int entity) +{ + OnEntitySpawned_MapTriggers(entity); + OnEntitySpawned_MapButtons(entity); + OnEntitySpawned_MapStarts(entity); + OnEntitySpawned_MapZones(entity); +} + +public void OnEntitySpawnedPost(int entity) +{ + OnEntitySpawnedPost_MapStarts(entity); + OnEntitySpawnedPost_MapEnd(entity); +} + +public void OnClientConnected(int client) +{ + OnClientConnected_Triggerfix(client); +} + +public void OnRoundStart(Event event, const char[] name, bool dontBroadcast) // round_start post no copy hook +{ + if (event == INVALID_HANDLE) + { + OnRoundStart_Timer(); + OnRoundStart_ForceAllTalk(); + OnRoundStart_Demofix(); + return; + } + else + { + char objective[64]; + event.GetString("objective", objective, sizeof(objective)); + /* + External plugins that record GOTV demos can call round_start event to fix demo corruption, + which happens to stop the players' timer. GOKZ should only react on real round start events only. + */ + if (IsRealObjective(objective)) + { + OnRoundStart_Timer(); + OnRoundStart_ForceAllTalk(); + OnRoundStart_Demofix(); + } + } +} + +public Action CS_OnTerminateRound(float &delay, CSRoundEndReason &reason) +{ + return Plugin_Handled; +} + +public void GOKZ_OnModeUnloaded(int mode) +{ + OnModeUnloaded_Options(mode); +} + +public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu) +{ + OnOptionsMenuCreated_OptionsMenu(); +} + +public void GOKZ_OnOptionsMenuReady(TopMenu topMenu) +{ + OnOptionsMenuReady_OptionsMenu(); +} + + + +// =====[ PRIVATE ]===== + +static void CreateConVars() +{ + AutoExecConfig_SetFile("gokz-core", "sourcemod/gokz"); + AutoExecConfig_SetCreateFile(true); + + gCV_gokz_chat_prefix = AutoExecConfig_CreateConVar("gokz_chat_prefix", "{green}KZ {grey}| ", "Chat prefix used for GOKZ messages."); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + gCV_sv_full_alltalk = FindConVar("sv_full_alltalk"); + + // Remove unwanted flags from constantly changed mode convars - replication is done manually in mode plugins + for (int i = 0; i < MODECVAR_COUNT; i++) + { + FindConVar(gC_ModeCVars[i]).Flags &= ~FCVAR_NOTIFY; + FindConVar(gC_ModeCVars[i]).Flags &= ~FCVAR_REPLICATED; + } +} + +static void HookEvents() +{ + AddCommandsListeners(); + + HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post); + HookEvent("player_team", OnPlayerJoinTeam, EventHookMode_Pre); + HookEvent("player_death", OnPlayerDeath, EventHookMode_Pre); + HookEvent("player_jump", OnPlayerJump); + HookEvent("round_start", OnRoundStart, EventHookMode_PostNoCopy); + AddNormalSoundHook(OnNormalSound); + + GameData gameData = new GameData("sdktools.games"); + int offset; + + // Setup DHooks OnTeleport for players + offset = gameData.GetOffset("Teleport"); + gH_DHooks_OnTeleport = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnTeleport); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_ObjectPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr); + DHookAddParam(gH_DHooks_OnTeleport, HookParamType_Bool); + + gameData = new GameData("sdktools.games/engine.csgo"); + offset = gameData.GetOffset("SetEntityModel"); + gH_DHooks_SetModel = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnSetModel); + DHookAddParam(gH_DHooks_SetModel, HookParamType_CharPtr); + + delete gameData; +} + +static void HookClientEvents(int client) +{ + DHookEntity(gH_DHooks_OnTeleport, true, client); + DHookEntity(gH_DHooks_SetModel, true, client); + SDKHook(client, SDKHook_SpawnPost, OnCSPlayerSpawnPost); + SDKHook(client, SDKHook_PreThinkPost, OnClientPreThinkPost); +} + +static void UpdateTrackingVariables(int client, int cmdnum, int buttons) +{ + if (IsPlayerAlive(client)) + { + gB_OldOnGround[client] = Movement_GetOnGround(client); + } + + gI_OldButtons[client] = buttons; + + if (gB_OriginTeleported[client] || gB_VelocityTeleported[client]) + { + gI_TeleportCmdNum[client] = cmdnum; + } + gB_OriginTeleported[client] = false; + gB_VelocityTeleported[client] = false; +} + +static bool IsRealObjective(char[] objective) +{ + return StrEqual(objective, "PRISON ESCAPE") || StrEqual(objective, "DEATHMATCH") + || StrEqual(objective, "BOMB TARGET") || StrEqual(objective, "HOSTAGE RESCUE"); +} \ No newline at end of file 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(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({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(OPTION_COUNT); option++) + { + if (option == view_as(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(OPTION_COUNT); i++) + { + if (topobj_id == itemsGeneral[i]) + { + option = view_as