From aef0d1c1268ab7d4bc18996c9c6b4da16a40aadc Mon Sep 17 00:00:00 2001 From: navewindre Date: Mon, 4 Dec 2023 18:06:10 +0100 Subject: bbbbbbbbwaaaaaaaaaaa --- sourcemod-1.5-dev/scripting/ljstats.sp | 218 ++- sourcemod-1.5-dev/scripting/stats.sp | 168 +- 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 +++ web/index.php | 22 +- web/minecraft.php | 2 +- 213 files changed, 60783 insertions(+), 96 deletions(-) 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 diff --git a/sourcemod-1.5-dev/scripting/ljstats.sp b/sourcemod-1.5-dev/scripting/ljstats.sp index 8167430..f220d39 100644 --- a/sourcemod-1.5-dev/scripting/ljstats.sp +++ b/sourcemod-1.5-dev/scripting/ljstats.sp @@ -14,6 +14,7 @@ #define MAX(%0,%1) (%0 < %1 ? %1 : %0) #define LJSTATS_VERSION "2.0.1" +#define MAX_JUMP_TICKS 132 // 2 sec #define LJTOP_DIR "configs/ljstats/" #define LJTOP_FILE "ljtop.txt" @@ -33,8 +34,8 @@ #define BJ_HEIGHT_DELTA_MAX 2.0 #define LAJ_HEIGHT_DELTA_MIN -6.0 #define LAJ_HEIGHT_DELTA_MAX 0.0 -#define JB_HEIGHT_DELTA_MIN -4.0 -#define JB_HEIGHT_DELTA_MAX 1.0 +#define JB_HEIGHT_DELTA_MIN -1.0 +#define JB_HEIGHT_DELTA_MAX 1.5 #define HUD_HINT_SIZE 256 #define STRAFE_TRAINER_TICKS 9 @@ -57,6 +58,7 @@ enum PlayerState bool:bBeam, bool:bDeadstrafe, bool:bSound, + bool:bSyncStats, bool:bBlockMode, nVerbosity, bool:bShowAllJumps, @@ -110,6 +112,8 @@ enum PlayerState Float:fStrafeSync[MAX_STRAFES], nStrafeTicks[MAX_STRAFES], nStrafeTicksSynced[MAX_STRAFES], + nMoveDir[MAX_JUMP_TICKS], + nMouseDir[MAX_JUMP_TICKS], nTotalTicks, Float:fTotalAngle, Float:fSyncedAngle, @@ -405,6 +409,7 @@ new Handle:g_hCookieShowPrestrafeHint = INVALID_HANDLE; new Handle:g_hCookiePersonalBest = INVALID_HANDLE; new Handle:g_hCookieStrafeTrainer = INVALID_HANDLE; new Handle:g_hCookieSpeedometer = INVALID_HANDLE; +new Handle:g_hCookieSyncStats = INVALID_HANDLE; new g_ColorMin[3] = {0xAD, 0xD8, 0xE6}; // Lightblue! new g_ColorMax[3] = {0x00, 0x00, 0xFF}; @@ -528,6 +533,7 @@ public OnPluginStart() RegConsoleCmd("sm_ljsound", Command_LJSound); RegConsoleCmd("sm_ljver", Command_LJVersion); RegConsoleCmd("sm_ljversion", Command_LJVersion); + RegConsoleCmd("sm_syncstats", Command_SyncStats); RegConsoleCmd("sm_ljtop", Command_LJTop); #if defined LJSERV RegConsoleCmd("sm_wr", Command_LJTop); @@ -564,7 +570,8 @@ public OnPluginStart() g_hCookiePersonalBest = RegClientCookie("ljstats_personalbest", "ljstats_personalbest", CookieAccess_Private); g_hCookieStrafeTrainer = RegClientCookie("ljstats_strafetrainer", "ljstats_strafetrainer", CookieAccess_Private); g_hCookieSpeedometer = RegClientCookie("ljstats_speedometer", "ljstats_speedometer", CookieAccess_Private); - + g_hCookieSyncStats = RegClientCookie("ljstats_syncstats", "ljstats_syncstats", CookieAccess_Private); + for(new i = 1; i < MaxClients; i++) { if(IsClientInGame(i)) @@ -1053,6 +1060,19 @@ public Action:Command_Delete(client, args) return Plugin_Handled; } +public Action:Command_SyncStats( client, args ) { + if ( g_PlayerStates[client][bSyncStats] ) { + g_PlayerStates[client][bSyncStats] = false; + PrintToChat( client, "\x04Sync stats are now : DISABLED" ); + } else { + g_PlayerStates[client][bSyncStats] = true; + PrintToChat( client, "\x04Sync stats are now : ENABLED" ); + } + + SetCookie( client, g_hCookieSyncStats, g_PlayerStates[client][bSyncStats] ); + return Plugin_Handled; +} + public Action:Command_LJHelp(client, args) { new Handle:hHelpPanel = CreatePanel(); @@ -1166,6 +1186,9 @@ public OnClientCookiesCached(client) GetClientCookie(client, g_hCookieStrafeTrainer, strCookie, sizeof(strCookie)); g_PlayerStates[client][bStrafeTrainer] = bool:StringToInt(strCookie); + + GetClientCookie(client, g_hCookieSyncStats, strCookie, sizeof(strCookie)); + g_PlayerStates[client][bSyncStats] = bool:StringToInt(strCookie); GetClientCookie(client, g_hCookieBeam, strCookie, sizeof(strCookie)); g_PlayerStates[client][bBeam] = bool:StringToInt(strCookie); @@ -1253,6 +1276,9 @@ ShowSettingsPanel(client) Format(buf, sizeof(buf), "Strafe trainer: %s", g_PlayerStates[client][bStrafeTrainer] ? "On" : "Off"); AddMenuItem(hMenu, "strafetrainer", buf); + + Format(buf, sizeof(buf), "Sync stats: %s", g_PlayerStates[client][bSyncStats] ? "On" : "Off"); + AddMenuItem(hMenu, "syncstats", buf); DisplayMenu(hMenu, client, 0); } @@ -1356,6 +1382,9 @@ public SettingsMenuHandler(Handle:hMenu, MenuAction:ma, client, nItem) PrintToChat( client, "Strafe trainer is now %s", g_PlayerStates[client][bStrafeTrainer] ? "ENABLED" : "DISABLED" ); ShowSettingsPanel(client); } + else if(!strcmp(strInfo, "syncstats")) { + Command_SyncStats( client, 0 ); + } } case MenuAction_End: @@ -2473,13 +2502,8 @@ StrafeTrainer( client, bool: onGround, Float:angles[3], Float:velocity[3] ) { b = 0; } - new Handle:hText = CreateHudSynchronizer(); - if(hText != INVALID_HANDLE) - { - SetHudTextParams(-1.0, 0.2, GetTickInterval() * (STRAFE_TRAINER_TICKS+1), r, g, b, 255, 0, 0.0, 0.0, 0.1); - ShowSyncHudText(client, hText, msg); - CloseHandle(hText); - } + SetHudTextParams(-1.0, 0.2, GetTickInterval() * (STRAFE_TRAINER_TICKS+1), r, g, b, 255, 0, 0.0, 0.0, 0.1); + ShowHudText(client, 0, msg); g_PlayerStates[client][nTrainerTicks] = 0; } @@ -2538,12 +2562,12 @@ Speedometer( client, bool: bJump, bool: bGround, bool: bIsDucking, Float:velocit switch( g_PlayerStates[client][nSpeedometer] ) { case 1: { - SetHudTextParams(-1.0, 0.325, 0.05, r, g, b, 255, 0, 0.0, 0.0, 0.0); + SetHudTextParams(-1.0, 0.325, 0.1, r, g, b, 255, 0, 0.0, 0.0, 0.0); ShowHudText(client, 1, sBuffer); } case 2: { - SetHudTextParams(-1.0, 0.85, 0.05, r, g, b, 255, 0, 0.0, 0.0, 0.0); + SetHudTextParams(-1.0, 0.85, 0.1, r, g, b, 255, 0, 0.0, 0.0, 0.0); ShowHudText(client, 1, sBuffer); } } @@ -2723,6 +2747,11 @@ PlayerJump(client, JUMP_TYPE:JumpType2 = JT_LONGJUMP) g_PlayerStates[client][nStrafeTicks][i] = 0; g_PlayerStates[client][nStrafeTicksSynced][i] = 0; } + + for( new i = 0; i < MAX_JUMP_TICKS; ++i ) { + g_PlayerStates[client][nMouseDir][i] = 0; + g_PlayerStates[client][nMoveDir][i] = 0; + } // Reset stuff g_PlayerStates[client][JumpDir] = JD_NONE; @@ -2812,7 +2841,9 @@ PlayerJump(client, JUMP_TYPE:JumpType2 = JT_LONGJUMP) new Float:vVel[3]; GetEntPropVector(client, Prop_Data, "m_vecVelocity", vVel); - vOrigin[2] += vVel[2] * GetTickInterval(); + // ducking lowers u by 8.5 units + if( GetEntProp(client, Prop_Send, "m_bDucking", 1) ) + vOrigin[2] -= 8.5; } Array_Copy(vOrigin, g_PlayerStates[client][vJumpOrigin], 3); @@ -3358,11 +3389,11 @@ _OnPlayerRunCmd(client, buttons, const Float:vOrigin[3], const Float:vAngles[3], new Float:fVelDelta = GetSpeed(client) - GetVSpeed(v); new Float:fAngleDelta = fmod((FloatAbs(vAngles[1] - v2[1]) + 180.0), 360.0) - 180.0; - g_PlayerStates[client][nStrafeTicks][g_PlayerStates[client][nStrafes] - 1]++; g_PlayerStates[client][fTotalAngle] += fAngleDelta; + new tick = g_PlayerStates[client][nTotalTicks]; if(fVelDelta > 0.0) { g_PlayerStates[client][fStrafeGain][g_PlayerStates[client][nStrafes] - 1] += fVelDelta; @@ -3371,11 +3402,44 @@ _OnPlayerRunCmd(client, buttons, const Float:vOrigin[3], const Float:vAngles[3], g_PlayerStates[client][nStrafeTicksSynced][g_PlayerStates[client][nStrafes] - 1]++; g_PlayerStates[client][fSyncedAngle] += fAngleDelta; + new Float:delta = vAngles[1] - v2[1]; + while(delta < -180.0) + delta += 360.0; + while(delta > 180.0) + delta -= 360.0; + + if( tick < MAX_JUMP_TICKS ) + g_PlayerStates[client][nMouseDir][tick] = delta > 0.0 ? -1 : 1; } else { g_PlayerStates[client][fStrafeLoss][g_PlayerStates[client][nStrafes] - 1] -= fVelDelta; g_PlayerStates[client][fLoss] -= fVelDelta; + if( tick < MAX_JUMP_TICKS ) + g_PlayerStates[client][nMouseDir][tick] = 0; + } + + if( tick < MAX_JUMP_TICKS ) { + if( g_PlayerStates[client][JumpDir] == JD_SIDEWAYS ) { + if( !nButtonCount ) + g_PlayerStates[client][nMoveDir][tick] = 0; + else if( g_PlayerStates[client][CurStrafeDir] == SD_W ) + g_PlayerStates[client][nMoveDir][tick] = -1; + else if( g_PlayerStates[client][CurStrafeDir] == SD_S ) + g_PlayerStates[client][nMoveDir][tick] = 1; + else + g_PlayerStates[client][nMoveDir][tick] = 0; + } + else { + if( !nButtonCount ) + g_PlayerStates[client][nMoveDir][tick] = 0; + else if( g_PlayerStates[client][CurStrafeDir] == SD_A ) + g_PlayerStates[client][nMoveDir][tick] = -1; + else if( g_PlayerStates[client][CurStrafeDir] == SD_D ) + g_PlayerStates[client][nMoveDir][tick] = 1; + else + g_PlayerStates[client][nMoveDir][tick] = 0; + } } } @@ -3454,6 +3518,130 @@ PrintPrestrafeHint(client) PrintHintText(client, strHint); } +public PrintSyncStats(client) { + new String:strLeft[256]; + new String:strRight[256]; + new String:strMouseLeft[256]; + new String:strMouseRight[256]; + + new String:strFull[1024]; + + if( g_PlayerStates[client][nStrafes] == 0 ) + return; + if( g_PlayerStates[client][nTotalTicks] < 10 ) + return; + + Format( strLeft, sizeof( strLeft ), "[ " ); + Format( strRight, sizeof( strRight ), "[ " ); + Format( strMouseLeft, sizeof( strMouseLeft ), "[ " ); + Format( strMouseRight, sizeof( strMouseRight ), "[ " ); + + for( new i = 0; i < g_PlayerStates[client][nTotalTicks]; ++i ) { + if( g_PlayerStates[client][nMouseDir][i] == -1 ) { + Append( strMouseLeft, sizeof( strMouseLeft ), "|" ); + Append( strMouseRight, sizeof( strMouseRight ), " " ); + } + else if( g_PlayerStates[client][nMouseDir][i] == 1 ) { + Append( strMouseLeft, sizeof( strMouseLeft ), " " ); + Append( strMouseRight, sizeof( strMouseRight ), "|" ); + } + else { + Append( strMouseLeft, sizeof( strMouseLeft ), " " ); + Append( strMouseRight, sizeof( strMouseRight ), " " ); + } + + if( g_PlayerStates[client][nMoveDir][i] == -1 ) { + Append( strLeft, sizeof( strLeft ), "|" ); + Append( strRight, sizeof( strRight ), " " ); + } + else if( g_PlayerStates[client][nMoveDir][i] == 1 ) { + Append( strLeft, sizeof( strLeft ), " " ); + Append( strRight, sizeof( strRight ), "|" ); + } + else { + Append( strLeft, sizeof( strLeft ), " " ); + Append( strRight, sizeof( strRight ), " " ); + } + } + + Format( strLeft, sizeof( strLeft ), "%s ]", strLeft ); + Format( strRight, sizeof( strRight ), "%s ]", strRight ); + Format( strMouseLeft, sizeof( strMouseLeft ), "%s ]", strMouseLeft ); + Format( strMouseRight, sizeof( strMouseRight ), "%s ]", strMouseRight ); + + if( g_PlayerStates[client][JumpDir] == JD_SIDEWAYS ) { + Format( strFull, sizeof( strFull ), "W: %s\nS: %s\nL: %s\nR: %s", strLeft, strRight, strMouseLeft, strMouseRight ); + } + else { + Format( strFull, sizeof( strFull ), "A: %s\nD: %s\nL: %s\nR: %s", strLeft, strRight, strMouseLeft, strMouseRight ); + } + + PrintToConsole( client, strFull ); + + if( g_PlayerStates[client][bSyncStats] ) { + Format( strLeft, sizeof( strLeft ), "[ " ); + Format( strRight, sizeof( strRight ), "[ " ); + Format( strMouseLeft, sizeof( strMouseLeft ), "[ " ); + Format( strMouseRight, sizeof( strMouseRight ), "[ " ); + + new String:char1[] = "|"; + new String:char2[] = "_"; + new String:strFull2[1024]; + + for( new i = 0; i < g_PlayerStates[client][nTotalTicks]; ++i ) { + if( g_PlayerStates[client][nMouseDir][i] == -1 ) { + Append( strMouseLeft, sizeof( strMouseLeft ), char1 ); + Append( strMouseRight, sizeof( strMouseRight ), char2 ); + } + else if( g_PlayerStates[client][nMouseDir][i] == 1 ) { + Append( strMouseLeft, sizeof( strMouseLeft ), char2 ); + Append( strMouseRight, sizeof( strMouseRight ), char1 ); + } + else { + Append( strMouseLeft, sizeof( strMouseLeft ), char2 ); + Append( strMouseRight, sizeof( strMouseRight ), char2 ); + } + + if( g_PlayerStates[client][nMoveDir][i] == -1 ) { + Append( strLeft, sizeof( strLeft ), char1 ); + Append( strRight, sizeof( strRight ), char2 ); + } + else if( g_PlayerStates[client][nMoveDir][i] == 1 ) { + Append( strLeft, sizeof( strLeft ), char2 ); + Append( strRight, sizeof( strRight ), char1 ); + } + else { + Append( strLeft, sizeof( strLeft ), char2 ); + Append( strRight, sizeof( strRight ), char2 ); + } + } + + Format( strLeft, sizeof( strLeft ), "%s ]", strLeft ); + Format( strRight, sizeof( strRight ), "%s ]", strRight ); + Format( strMouseLeft, sizeof( strMouseLeft ), "%s ]", strMouseLeft ); + Format( strMouseRight, sizeof( strMouseRight ), "%s ]", strMouseRight ); + + if( g_PlayerStates[client][JumpDir] == JD_SIDEWAYS ) { + Format( strFull, sizeof( strFull ), "W: %s\nS: %s\n", strLeft, strRight ); + } + else { + Format( strFull, sizeof( strFull ), "A: %s\nD: %s\n", strLeft, strRight ); + } + + Format( strFull2, sizeof( strFull2 ), "L: %s\nR: %s", strMouseLeft, strMouseRight ); + + new Handle:hText = CreateHudSynchronizer(); + if(hText != INVALID_HANDLE) + { + SetHudTextParams(-1.0, 0.06, 3.0, 255, 255, 255, 255, 0, 0.0, 0.15, 0.5); + ShowHudText(client, 2, strFull); + SetHudTextParams(-1.0, 0.14, 3.0, 180, 180, 255, 255, 0, 0.0, 0.15, 0.5); + ShowHudText(client, 3, strFull2); + CloseHandle(hText); + } + } +} + PlayerLand(client) { g_PlayerStates[client][bOnGround] = true; @@ -3463,7 +3651,7 @@ PlayerLand(client) if(!g_PlayerStates[client][bLJEnabled] && !g_PlayerStates[client][nSpectators] || !g_PlayerStates[client][bShowBhopStats] && g_PlayerStates[client][nBhops] > 1) return; - + PrintSyncStats( client ); // Final CheckValidJump //CheckValidJump(client); diff --git a/sourcemod-1.5-dev/scripting/stats.sp b/sourcemod-1.5-dev/scripting/stats.sp index 47030d4..94f3598 100644 --- a/sourcemod-1.5-dev/scripting/stats.sp +++ b/sourcemod-1.5-dev/scripting/stats.sp @@ -143,13 +143,16 @@ new g_LJTopMax[LT_END] = { 0, ... }; new g_statsTOP = 0; new g_playerStats[MAXPLAYERS+1][PlayerStats]; new g_playerTop[STATSTOP_NUM_ENTRIES][PlayerStats]; +new g_displayedStats[MAXPLAYERS+1] = { false, ... }; public OnPluginStart() { CreateTimer( 1.0, Timer_PlaytimeTick, _ ); RegConsoleCmd( "sm_stats", Command_Stats, "shows stats" ); RegAdminCmd( "sm_savestats", Command_SaveStats, ADMFLAG_ROOT, "saves stats" ); ResetPlayerStates(); - + + HookEvent( "player_death", Event_PlayerDeath ); + CreateTimer( 1.0, Timer_PlaytimeTick, _, TIMER_REPEAT ); } @@ -172,37 +175,22 @@ public FindKDTopIndex( Float:kd ) { if( !kd ) return g_statsTOP; - if( kd > g_playerTop[0][plKDRatio] ) + if( kd >= g_playerTop[0][plKDRatio] ) return 0; - if( g_statsTOP > 0 && kd < g_playerTop[g_statsTOP - 1][plKDRatio] ) - return g_statsTOP; + new top = g_statsTOP; + if( top > STATSTOP_NUM_ENTRIES ) + top = STATSTOP_NUM_ENTRIES; - for( new i = 0; i < 3; ++i ) { - if( g_playerTop[i][plKDRatio] > kd ) + if( top >= 0 && kd < g_playerTop[top - 1][plKDRatio] ) + return g_statsTOP; + + for( new i = 0; i < top; ++i ) { + if( g_playerTop[i][plKDRatio] < kd ) return i; } - new i = g_statsTOP / 2; - new denominator = 4; - new bool:flip = false; - - for( ;; ) { - if( g_playerTop[i][plKDRatio] > kd && g_playerTop[i + 1][plKDRatio] < kd ) - return i; - else if( g_playerTop[i][plKDRatio] > kd ) - flip = false; - else - flip = true; - - if( denominator < g_statsTOP ) - denominator *= 2; - - if( flip ) - i += (g_statsTOP / denominator); - else - i -= (g_statsTOP / denominator); - } + return g_statsTOP; } public ResetPlayerState( i ) { @@ -396,10 +384,10 @@ public Float:GetLJScore( client, table ) { else if( pos == 3 ) score = 500.0; - new Float:div = Float:g_LJTopMax[table]; + new Float:div = float(g_LJTopMax[table]); if( div < 1.0 ) div = 1.0; - score += (500 - (Float:pos / div * 500)) * multiplier; + score += (500.0 - (float(pos) / div * 500.0)) * multiplier; return score; } @@ -409,28 +397,28 @@ public Float:GetKDScore( client ) { new Float:kd = g_playerStats[client][plKDRatio]; new kdPlacement = FindKDTopIndex( kd ) + 1; if( kdPlacement == 1 ) - points += 1000; + points += 1000.0; else if( kdPlacement == 2 ) - points += 750; - else if( kdPlacement == 3) - points += 500; + points += 750.0; + else if( kdPlacement == 3 ) + points += 500.0; - new Float:num = Float:kdPlacement; - new Float:den = Float:g_statsTOP; + new Float:num = float(kdPlacement); + new Float:den = float(g_statsTOP); if( den < 1.0 ) den = 1.0; - points += 500 - (num / den * 500); + points += 500.0 - ( num / den ) * 500.0; return points; } public Float:GetPlayerPoints( client ) { new Float:points = 0.0; - points += g_playerStats[client][plKills] * 1; - points -= g_playerStats[client][plDeaths] * 0.9; - points += g_playerStats[client][plRoundwins] * 0.5; - points -= g_playerStats[client][plRoundlosses] * 0.5; + points += float(g_playerStats[client][plKills]) * 1; + points -= float(g_playerStats[client][plDeaths]) * 0.9; + points += float(g_playerStats[client][plRoundwins]) * 0.5; + points -= float(g_playerStats[client][plRoundlosses]) * 0.5; points += GetLJScore( client, _:LT_LJ ); points += GetLJScore( client, _:LT_CJ ); points += GetLJScore( client, _:LT_BJ ); @@ -550,7 +538,7 @@ public SaveStatsForTop( iClient, Handle:hndl ) { SQL_FetchStringByName( hndl, "name", name, sizeof(name) ); strcopy( g_playerTop[iClient][plName], sizeof(name), name ); decl String:steamid[32]; - SQL_FetchStringByName( hndl, "steamid", steamid, sizeof(steamid) );dd + SQL_FetchStringByName( hndl, "steamid", steamid, sizeof(steamid) ); strcopy( g_playerTop[iClient][plSteamid], sizeof(steamid), steamid ); } @@ -560,15 +548,20 @@ public LoadStatsDBCallback( Handle:owner, Handle:hndl, String:error[], any:pack return; } - g_statsTOP = SQL_GetRowCount( hndl ); - for( new i = 0; i < g_statsTOP; i++ ) { + new rows = SQL_GetRowCount( hndl ); + new it = 0; + if( !pack ) + g_statsTOP = rows; + for( new i = 0; i < rows; i++ ) { SQL_FetchRow( hndl ); decl String:steamid[32]; SQL_FetchStringByName( hndl, "steamid", steamid, sizeof(steamid) ); + if( steamid[0] != 'S' ) + continue; new iClient = 0; for( new i2 = 1; i2 < GetMaxClients(); ++i2 ) { - if( !IsClientConnected( i2 ) || !IsClientInGame( i2 ) ) + if( !IsClientConnected( i2 ) ) continue; new String:playerSteamID[32]; @@ -582,16 +575,20 @@ public LoadStatsDBCallback( Handle:owner, Handle:hndl, String:error[], any:pack if( iClient != 0 ) { SaveStatsForClient( iClient, hndl ); strcopy( g_playerStats[iClient][plSteamid], sizeof(steamid), steamid ); - + LogMessage( "Loading db for client %s pack: %d", steamid, !!pack ); if( !pack ) - g_playerStats[iClient][plStatPos] = i; + g_playerStats[iClient][plStatPos] = it; } - if( i >= STATSTOP_NUM_ENTRIES - 1 || !!pack ) + if( i >= STATSTOP_NUM_ENTRIES - 1 || !!pack ) { + ++it; continue; + } - g_playerTop[i][plStatPos] = i; - SaveStatsForTop( i, hndl ); + g_playerTop[i][plStatPos] = it; + SaveStatsForTop( it, hndl ); + LogMessage( "Loading db for top %s (%s)", g_playerTop[it][plName], steamid ); + ++it; } } @@ -619,21 +616,17 @@ public LoadStatsDB() { SQL_TQuery(g_statsDB, CreateStatsDBCallback, "CREATE TABLE IF NOT EXISTS playerstats (steamid VARCHAR(32) NOT NULL, name VARCHAR(64) NOT NULL, kills INT NOT NULL, deaths INT NOT NULL, kdratio FLOAT NOT NULL, totalpoints INT NOT NULL, playtime INT NOT NULL, alivetime INT NOT NULL, deadtime INT NOT NULL, ljtop FLOAT NOT NULL, ljtoppos INT NOT NULL, swljtop FLOAT NOT NULL, swljtoppos INT NOT NULL, bwljtop FLOAT NOT NULL, bwljtoppos INT NOT NULL, cjtop FLOAT NOT NULL, cjtoppos INT NOT NULL, lajtop FLOAT NOT NULL, lajtoppos INT NOT NULL, wjtop FLOAT NOT NULL, wjtoppos INT NOT NULL, jbtop FLOAT NOT NULL, jbtoppos INT NOT NULL, roundwins INT NOT NULL, roundlosses INT NOT NULL, PRIMARY KEY (steamid))"); } -public LoadStatsDBForUser( client ) { - if( g_statsDB == INVALID_HANDLE ) { - LogMessage( "db invalid" ); - return; - } - - if( !IsClientConnected( client ) || !IsClientInGame( client ) || IsFakeClient( client ) ) { - LogMessage( "client not connected" ); - return; - } - - decl String:steamid[32]; +public FindEntryForUser( client, bool:update ) { + new String:steamid[32]; GetClientAuthString( client, steamid, sizeof(steamid) ); - for( new i = 0; i < g_statsTOP; ++i ) { - if( !strcmp( g_playerTop[i][plSteamid], steamid ) ) { + new top = g_statsTOP > STATSTOP_NUM_ENTRIES ? STATSTOP_NUM_ENTRIES : g_statsTOP; + for( new i = 0; i < top; ++i ) { + new cmp = strcmp( g_playerTop[i][plSteamid], steamid ); + if( cmp == 0 ) { + g_playerStats[client][plStatPos] = i; + if( !update ) + return; + g_playerStats[client][plKills] = g_playerTop[i][plKills]; g_playerStats[client][plDeaths] = g_playerTop[i][plDeaths]; g_playerStats[client][plKDRatio] = g_playerTop[i][plKDRatio]; @@ -660,10 +653,27 @@ public LoadStatsDBForUser( client ) { decl String:name[64]; strcopy( name, sizeof(name), g_playerTop[i][plName] ); strcopy( g_playerStats[client][plName], sizeof(name), name ); - - g_playerStats[client][plStatPos] = i; + + LogMessage( "found existing top: %d", i ); + break; } } +} + +public LoadStatsDBForUser( client ) { + if( g_statsDB == INVALID_HANDLE ) { + LogMessage( "db invalid" ); + return; + } + + if( !IsClientConnected( client ) || IsFakeClient( client ) ) { + return; + } + + FindEntryForUser( client, true ); + + new String:steamid[32]; + GetClientAuthString( client, steamid, sizeof(steamid) ); decl String:query[1024]; Format( query, sizeof(query), "SELECT * FROM playerstats WHERE steamid='%s'", steamid ); @@ -737,7 +747,7 @@ public SaveStatsDB() { return; for( new i = 1; i < MaxClients; ++i ) { - if( !IsClientConnected( i ) || !IsClientInGame( i ) || IsFakeClient( i ) ) + if( !IsClientConnected( i ) || IsFakeClient( i ) ) continue; SaveStatsDBForPlayer( i ); @@ -797,7 +807,7 @@ public Action:OnClientSayCommand( client, const String:command[], const String:a new String:color[16]; strcopy( color, sizeof(color), g_playerRankColors[rank] ); - Format( fullOut, sizeof(fullOut), "%s%s[%s%s%s]", fullOut, bracketColor, color, rankString, bracketColor ); + Format( fullOut, sizeof(fullOut), "%s%s[%s%s%s] ", fullOut, bracketColor, color, rankString, bracketColor ); } new String:name[64]; @@ -990,6 +1000,10 @@ public DisplayTopStats( client, target ) { new String:name[64]; new String:steamId[32]; new String:color[16]; + new top = g_statsTOP > STATSTOP_NUM_ENTRIES ? STATSTOP_NUM_ENTRIES : g_statsTOP; + if( target > top ) + CPrintToChat( client, "{fuchsia}There are only {default}%d {fuchsia}players in the database.", top ); + strcopy( name, sizeof(name), g_playerTop[client][plName] ); strcopy( steamId, sizeof(steamId), g_playerTop[client][plSteamid] ); @@ -1009,7 +1023,7 @@ public DisplayTopStats( client, target ) { Format( chatOutput, sizeof(chatOutput), "%s{white}%s {default}({green}%s{default}) is ranked %s#%d{default}/{green}%d {default}\n", chatOutput, name, steamId, color, pos + 1, g_statsTOP ); Format( chatOutput, sizeof(chatOutput), "%s{white}%s{default}'s points : {green}%d\n", chatOutput, name, g_playerTop[client][plTotalPoints] ); - CPrintToChat( target, chatOutput ); + CPrintToChatEx( target, target, chatOutput ); } public Action:Command_Stats( client, args ) { @@ -1061,7 +1075,10 @@ public Action:Timer_PlaytimeTick( Handle: timer, any: unused ) { for( new i = 1; i < MaxClients; ++i ) { if( !IsClientConnected( i ) || !IsClientInGame( i ) || IsFakeClient( i ) ) continue; - + + if( g_playerStats[i][plStatPos] == -1 ) + FindEntryForUser( i, g_playerStats[i][plSteamid][0] == '\0' ); + g_playerStats[i][plPlaytime]++; if( IsPlayerAlive( i ) ) g_playerStats[i][plAlivetime]++; @@ -1093,16 +1110,14 @@ public Action:Event_PlayerDeath( Handle:event, const String:name[], bool:dontBro new killer = GetClientOfUserId( killeruid ); new _target = GetClientOfUserId( targetuid ); - if( !killer || !_target ) + if( !killer || !_target || killer == _target ) return Plugin_Continue; if( killer && IsClientConnected( killer ) && !IsFakeClient( killer ) && !IsFakeClient( _target ) ) { - LogMessage( "killer kills: %d", g_playerStats[killer][plKills] ); g_playerStats[killer][plKills]++; } if( _target && IsClientConnected( _target ) && !IsFakeClient( _target ) ) { g_playerStats[_target][plDeaths]++; - LogMessage( "target deaths: %d", g_playerStats[_target][plDeaths] ); } new killerKills = g_playerStats[killer][plKills]; @@ -1110,13 +1125,16 @@ public Action:Event_PlayerDeath( Handle:event, const String:name[], bool:dontBro new targetKills = g_playerStats[_target][plKills]; new targetDeaths = g_playerStats[_target][plDeaths]; - if( targetDeaths <= 0 ) + if( targetDeaths <= 1 ) targetDeaths = 1; - if( killerDeaths <= 0 ) + if( killerDeaths <= 1 ) killerDeaths = 1; - g_playerStats[killer][plKDRatio] = Float:killerKills / Float:killerDeaths; - g_playerStats[_target][plKDRatio] = Float:targetKills / Float:targetDeaths; + new Float:killerKDRatio = float(killerKills) / float(killerDeaths); + new Float:targetKDRatio = float(targetKills) / float(targetDeaths); + + g_playerStats[killer][plKDRatio] = killerKDRatio; + g_playerStats[_target][plKDRatio] = targetKDRatio; if( !IsFakeClient( killer ) ) SaveStatsDBForPlayer( killer ); 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