From 5e2eb7d67ae933b7566f1944d0bb7744da03d586 Mon Sep 17 00:00:00 2001 From: aura Date: Tue, 17 Feb 2026 23:42:09 +0100 Subject: move source stuff to its own folder --- source/sourcemod/GPLv2.txt | 339 +++ source/sourcemod/GPLv3.txt | 674 +++++ source/sourcemod/LICENSE.txt | 36 + source/sourcemod/scripting/bot2player_public.sp | 527 ++++ source/sourcemod/scripting/compile.exe | Bin 0 -> 113664 bytes source/sourcemod/scripting/css-kztimer.sp | 984 +++++++ source/sourcemod/scripting/distbugfix.sp | 1592 ++++++++++ .../sourcemod/scripting/distbugfix/clientprefs.sp | 51 + source/sourcemod/scripting/game_manager.sp | 718 +++++ source/sourcemod/scripting/gem_damage_report.sp | 699 +++++ source/sourcemod/scripting/gokz-anticheat.sp | 318 ++ source/sourcemod/scripting/gokz-anticheat/api.sp | 174 ++ .../scripting/gokz-anticheat/bhop_tracking.sp | 336 +++ .../sourcemod/scripting/gokz-anticheat/commands.sp | 76 + source/sourcemod/scripting/gokz-chat.sp | 309 ++ source/sourcemod/scripting/gokz-core.sp | 543 ++++ source/sourcemod/scripting/gokz-core/commands.sp | 385 +++ source/sourcemod/scripting/gokz-core/demofix.sp | 110 + source/sourcemod/scripting/gokz-core/forwards.sp | 401 +++ .../sourcemod/scripting/gokz-core/map/buttons.sp | 138 + source/sourcemod/scripting/gokz-core/map/end.sp | 155 + .../sourcemod/scripting/gokz-core/map/mapfile.sp | 502 ++++ source/sourcemod/scripting/gokz-core/map/prefix.sp | 48 + source/sourcemod/scripting/gokz-core/map/starts.sp | 219 ++ .../sourcemod/scripting/gokz-core/map/triggers.sp | 855 ++++++ source/sourcemod/scripting/gokz-core/map/zones.sp | 183 ++ .../scripting/gokz-core/menus/mode_menu.sp | 40 + .../scripting/gokz-core/menus/options_menu.sp | 174 ++ source/sourcemod/scripting/gokz-core/misc.sp | 803 +++++ source/sourcemod/scripting/gokz-core/modes.sp | 106 + source/sourcemod/scripting/gokz-core/natives.sp | 647 ++++ source/sourcemod/scripting/gokz-core/options.sp | 438 +++ source/sourcemod/scripting/gokz-core/teamnumfix.sp | 68 + source/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 ++ source/sourcemod/scripting/gokz-core/triggerfix.sp | 622 ++++ source/sourcemod/scripting/gokz-errorboxfixer.sp | 89 + source/sourcemod/scripting/gokz-global.sp | 740 +++++ source/sourcemod/scripting/gokz-global/api.sp | 142 + .../sourcemod/scripting/gokz-global/ban_player.sp | 42 + source/sourcemod/scripting/gokz-global/commands.sp | 169 ++ .../sourcemod/scripting/gokz-global/maptop_menu.sp | 249 ++ source/sourcemod/scripting/gokz-global/points.sp | 147 + .../scripting/gokz-global/print_records.sp | 190 ++ source/sourcemod/scripting/gokz-global/send_run.sp | 143 + source/sourcemod/scripting/gokz-goto.sp | 231 ++ source/sourcemod/scripting/gokz-hud.sp | 334 +++ source/sourcemod/scripting/gokz-hud/commands.sp | 116 + source/sourcemod/scripting/gokz-hud/hide_weapon.sp | 30 + source/sourcemod/scripting/gokz-hud/info_panel.sp | 307 ++ source/sourcemod/scripting/gokz-hud/menu.sp | 96 + source/sourcemod/scripting/gokz-hud/natives.sp | 33 + source/sourcemod/scripting/gokz-hud/options.sp | 190 ++ .../sourcemod/scripting/gokz-hud/options_menu.sp | 181 ++ source/sourcemod/scripting/gokz-hud/racing_text.sp | 167 ++ .../sourcemod/scripting/gokz-hud/spectate_text.sp | 119 + source/sourcemod/scripting/gokz-hud/speed_text.sp | 141 + source/sourcemod/scripting/gokz-hud/timer_text.sp | 135 + source/sourcemod/scripting/gokz-hud/tp_menu.sp | 415 +++ source/sourcemod/scripting/gokz-jumpbeam.sp | 325 +++ source/sourcemod/scripting/gokz-jumpstats.sp | 216 ++ source/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 + .../scripting/gokz-jumpstats/options_menu.sp | 145 + source/sourcemod/scripting/gokz-localdb.sp | 188 ++ source/sourcemod/scripting/gokz-localdb/api.sp | 126 + .../sourcemod/scripting/gokz-localdb/commands.sp | 199 ++ .../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 ++ .../scripting/gokz-localdb/db/save_time.sp | 83 + .../scripting/gokz-localdb/db/set_cheater.sp | 64 + .../scripting/gokz-localdb/db/setup_client.sp | 99 + .../scripting/gokz-localdb/db/setup_database.sp | 34 + .../scripting/gokz-localdb/db/setup_map.sp | 71 + .../scripting/gokz-localdb/db/setup_map_courses.sp | 45 + source/sourcemod/scripting/gokz-localdb/db/sql.sp | 406 +++ .../scripting/gokz-localdb/db/timer_setup.sp | 167 ++ source/sourcemod/scripting/gokz-localdb/options.sp | 90 + source/sourcemod/scripting/gokz-localranks.sp | 263 ++ source/sourcemod/scripting/gokz-localranks/api.sp | 120 + .../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 + .../scripting/gokz-localranks/db/helpers.sp | 91 + .../scripting/gokz-localranks/db/js_top.sp | 286 ++ .../scripting/gokz-localranks/db/map_top.sp | 388 +++ .../scripting/gokz-localranks/db/player_top.sp | 165 ++ .../scripting/gokz-localranks/db/print_average.sp | 152 + .../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 + source/sourcemod/scripting/gokz-localranks/misc.sp | 319 ++ source/sourcemod/scripting/gokz-measure.sp | 82 + .../sourcemod/scripting/gokz-measure/commands.sp | 49 + .../scripting/gokz-measure/measure_menu.sp | 82 + .../sourcemod/scripting/gokz-measure/measurer.sp | 231 ++ source/sourcemod/scripting/gokz-mode-kztimer.sp | 709 +++++ source/sourcemod/scripting/gokz-mode-simplekz.sp | 846 ++++++ source/sourcemod/scripting/gokz-mode-vanilla.sp | 291 ++ source/sourcemod/scripting/gokz-momsurffix.sp | 724 +++++ source/sourcemod/scripting/gokz-paint.sp | 410 +++ source/sourcemod/scripting/gokz-pistol.sp | 303 ++ source/sourcemod/scripting/gokz-playermodels.sp | 198 ++ source/sourcemod/scripting/gokz-profile.sp | 396 +++ source/sourcemod/scripting/gokz-profile/options.sp | 128 + source/sourcemod/scripting/gokz-profile/profile.sp | 222 ++ source/sourcemod/scripting/gokz-quiet.sp | 151 + source/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 ++ source/sourcemod/scripting/gokz-quiet/options.sp | 206 ++ .../sourcemod/scripting/gokz-quiet/soundscape.sp | 30 + source/sourcemod/scripting/gokz-racing.sp | 174 ++ source/sourcemod/scripting/gokz-racing/announce.sp | 229 ++ source/sourcemod/scripting/gokz-racing/api.sp | 107 + source/sourcemod/scripting/gokz-racing/commands.sp | 47 + .../sourcemod/scripting/gokz-racing/duel_menu.sp | 534 ++++ source/sourcemod/scripting/gokz-racing/race.sp | 221 ++ .../sourcemod/scripting/gokz-racing/race_menu.sp | 464 +++ source/sourcemod/scripting/gokz-racing/racer.sp | 439 +++ source/sourcemod/scripting/gokz-replays.sp | 397 +++ source/sourcemod/scripting/gokz-replays/api.sp | 78 + .../sourcemod/scripting/gokz-replays/commands.sp | 55 + .../sourcemod/scripting/gokz-replays/controls.sp | 224 ++ source/sourcemod/scripting/gokz-replays/nav.sp | 97 + .../sourcemod/scripting/gokz-replays/playback.sp | 1501 ++++++++++ .../sourcemod/scripting/gokz-replays/recording.sp | 990 +++++++ .../scripting/gokz-replays/replay_cache.sp | 176 ++ .../scripting/gokz-replays/replay_menu.sp | 139 + source/sourcemod/scripting/gokz-saveloc.sp | 822 ++++++ source/sourcemod/scripting/gokz-slayonend.sp | 190 ++ source/sourcemod/scripting/gokz-spec.sp | 323 ++ source/sourcemod/scripting/gokz-tips.sp | 357 +++ source/sourcemod/scripting/gokz-tpanglefix.sp | 277 ++ source/sourcemod/scripting/include/GlobalAPI.inc | 822 ++++++ .../scripting/include/GlobalAPI/iterable.inc | 55 + .../scripting/include/GlobalAPI/request.inc | 185 ++ .../scripting/include/GlobalAPI/requestdata.inc | 534 ++++ .../scripting/include/GlobalAPI/responses.inc | 575 ++++ .../scripting/include/GlobalAPI/stocks.inc | 67 + source/sourcemod/scripting/include/SteamWorks.inc | 413 +++ source/sourcemod/scripting/include/admin.inc | 832 ++++++ source/sourcemod/scripting/include/adminmenu.inc | 152 + source/sourcemod/scripting/include/adt.inc | 40 + source/sourcemod/scripting/include/adt_array.inc | 469 +++ source/sourcemod/scripting/include/adt_stack.inc | 251 ++ source/sourcemod/scripting/include/adt_trie.inc | 332 +++ .../sourcemod/scripting/include/autoexecconfig.inc | 765 +++++ source/sourcemod/scripting/include/banning.inc | 156 + source/sourcemod/scripting/include/basecomm.inc | 109 + source/sourcemod/scripting/include/bitbuffer.inc | 470 +++ source/sourcemod/scripting/include/clientprefs.inc | 372 +++ source/sourcemod/scripting/include/clients.inc | 831 ++++++ source/sourcemod/scripting/include/colors.inc | 945 ++++++ .../sourcemod/scripting/include/commandfilters.inc | 171 ++ source/sourcemod/scripting/include/commandline.inc | 86 + source/sourcemod/scripting/include/console.inc | 780 +++++ source/sourcemod/scripting/include/convars.inc | 514 ++++ source/sourcemod/scripting/include/core.inc | 320 ++ source/sourcemod/scripting/include/cstrike.inc | 488 ++++ source/sourcemod/scripting/include/datapack.inc | 255 ++ source/sourcemod/scripting/include/dbi.inc | 1073 +++++++ source/sourcemod/scripting/include/dhooks.inc | 1100 +++++++ source/sourcemod/scripting/include/distbugfix.inc | 261 ++ source/sourcemod/scripting/include/entity.inc | 769 +++++ .../scripting/include/entity_prop_stocks.inc | 594 ++++ source/sourcemod/scripting/include/entitylump.inc | 157 + source/sourcemod/scripting/include/events.inc | 343 +++ source/sourcemod/scripting/include/files.inc | 628 ++++ source/sourcemod/scripting/include/float.inc | 460 +++ source/sourcemod/scripting/include/functions.inc | 631 ++++ source/sourcemod/scripting/include/gamechaos.inc | 20 + .../scripting/include/gamechaos/arrays.inc | 52 + .../scripting/include/gamechaos/client.inc | 300 ++ .../scripting/include/gamechaos/debug.inc | 19 + .../scripting/include/gamechaos/isvalidclient.inc | 16 + .../scripting/include/gamechaos/kreedzclimbing.inc | 226 ++ .../scripting/include/gamechaos/maths.inc | 362 +++ .../sourcemod/scripting/include/gamechaos/misc.inc | 245 ++ .../scripting/include/gamechaos/strings.inc | 367 +++ .../scripting/include/gamechaos/tempents.inc | 62 + .../scripting/include/gamechaos/tracing.inc | 242 ++ .../scripting/include/gamechaos/vectors.inc | 66 + source/sourcemod/scripting/include/geoip.inc | 241 ++ .../scripting/include/glib/addressutils.inc | 54 + .../scripting/include/glib/assertutils.inc | 61 + .../sourcemod/scripting/include/glib/memutils.inc | 232 ++ source/sourcemod/scripting/include/gokz.inc | 1097 +++++++ .../sourcemod/scripting/include/gokz/anticheat.inc | 168 ++ source/sourcemod/scripting/include/gokz/chat.inc | 45 + source/sourcemod/scripting/include/gokz/core.inc | 1920 ++++++++++++ source/sourcemod/scripting/include/gokz/global.inc | 317 ++ source/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 +++ .../scripting/include/gokz/localranks.inc | 176 ++ .../scripting/include/gokz/momsurffix.inc | 23 + source/sourcemod/scripting/include/gokz/paint.inc | 114 + source/sourcemod/scripting/include/gokz/pistol.inc | 93 + .../sourcemod/scripting/include/gokz/profile.inc | 291 ++ source/sourcemod/scripting/include/gokz/quiet.inc | 205 ++ source/sourcemod/scripting/include/gokz/racing.inc | 189 ++ .../sourcemod/scripting/include/gokz/replays.inc | 275 ++ .../sourcemod/scripting/include/gokz/slayonend.inc | 43 + source/sourcemod/scripting/include/gokz/tips.inc | 59 + .../scripting/include/gokz/tpanglefix.inc | 40 + .../sourcemod/scripting/include/gokz/version.inc | 12 + source/sourcemod/scripting/include/halflife.inc | 711 +++++ source/sourcemod/scripting/include/handles.inc | 98 + source/sourcemod/scripting/include/helpers.inc | 287 ++ source/sourcemod/scripting/include/json.inc | 473 +++ .../scripting/include/json/decode_helpers.inc | 312 ++ .../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 + source/sourcemod/scripting/include/json/object.inc | 1014 +++++++ .../scripting/include/json/string_helpers.inc | 77 + source/sourcemod/scripting/include/keyvalues.inc | 707 +++++ source/sourcemod/scripting/include/lang.inc | 134 + source/sourcemod/scripting/include/logging.inc | 135 + source/sourcemod/scripting/include/mapchooser.inc | 164 ++ source/sourcemod/scripting/include/menus.inc | 1154 ++++++++ source/sourcemod/scripting/include/morecolors.inc | 674 +++++ source/sourcemod/scripting/include/movement.inc | 530 ++++ source/sourcemod/scripting/include/movementapi.inc | 663 +++++ source/sourcemod/scripting/include/nextmap.inc | 82 + source/sourcemod/scripting/include/profiler.inc | 123 + source/sourcemod/scripting/include/protobuf.inc | 602 ++++ source/sourcemod/scripting/include/regex.inc | 291 ++ source/sourcemod/scripting/include/sdkhooks.inc | 457 +++ source/sourcemod/scripting/include/sdktools.inc | 232 ++ .../scripting/include/sdktools_client.inc | 52 + .../scripting/include/sdktools_engine.inc | 66 + .../scripting/include/sdktools_entinput.inc | 51 + .../scripting/include/sdktools_entoutput.inc | 108 + .../scripting/include/sdktools_functions.inc | 418 +++ .../scripting/include/sdktools_gamerules.inc | 199 ++ .../sourcemod/scripting/include/sdktools_hooks.inc | 112 + .../sourcemod/scripting/include/sdktools_sound.inc | 719 +++++ .../scripting/include/sdktools_stocks.inc | 75 + .../scripting/include/sdktools_stringtables.inc | 180 ++ .../scripting/include/sdktools_tempents.inc | 232 ++ .../scripting/include/sdktools_tempents_stocks.inc | 443 +++ .../sourcemod/scripting/include/sdktools_trace.inc | 716 +++++ .../scripting/include/sdktools_variant_t.inc | 93 + .../sourcemod/scripting/include/sdktools_voice.inc | 143 + source/sourcemod/scripting/include/smjansson.inc | 1328 +++++++++ source/sourcemod/scripting/include/smlib.inc | 32 + .../sourcemod/scripting/include/smlib/arrays.inc | 157 + .../sourcemod/scripting/include/smlib/clients.inc | 3077 ++++++++++++++++++++ .../sourcemod/scripting/include/smlib/colors.inc | 575 ++++ .../scripting/include/smlib/concommands.inc | 45 + .../sourcemod/scripting/include/smlib/convars.inc | 71 + source/sourcemod/scripting/include/smlib/crypt.inc | 625 ++++ source/sourcemod/scripting/include/smlib/debug.inc | 28 + .../scripting/include/smlib/dynarrays.inc | 24 + .../sourcemod/scripting/include/smlib/edicts.inc | 127 + .../sourcemod/scripting/include/smlib/effects.inc | 722 +++++ .../sourcemod/scripting/include/smlib/entities.inc | 2098 +++++++++++++ source/sourcemod/scripting/include/smlib/files.inc | 451 +++ source/sourcemod/scripting/include/smlib/game.inc | 57 + .../sourcemod/scripting/include/smlib/general.inc | 251 ++ source/sourcemod/scripting/include/smlib/math.inc | 346 +++ source/sourcemod/scripting/include/smlib/menus.inc | 37 + .../sourcemod/scripting/include/smlib/server.inc | 135 + source/sourcemod/scripting/include/smlib/sql.inc | 107 + .../sourcemod/scripting/include/smlib/strings.inc | 228 ++ source/sourcemod/scripting/include/smlib/teams.inc | 317 ++ .../sourcemod/scripting/include/smlib/vehicles.inc | 137 + .../sourcemod/scripting/include/smlib/weapons.inc | 393 +++ source/sourcemod/scripting/include/smlib/world.inc | 16 + source/sourcemod/scripting/include/sorting.inc | 169 ++ .../sourcemod/scripting/include/sourcebanspp.inc | 106 + .../scripting/include/sourcemod-colors.inc | 921 ++++++ source/sourcemod/scripting/include/sourcemod.inc | 772 +++++ source/sourcemod/scripting/include/string.inc | 590 ++++ source/sourcemod/scripting/include/testing.inc | 95 + source/sourcemod/scripting/include/textparse.inc | 255 ++ source/sourcemod/scripting/include/tf2.inc | 501 ++++ source/sourcemod/scripting/include/tf2_stocks.inc | 643 ++++ source/sourcemod/scripting/include/timers.inc | 200 ++ source/sourcemod/scripting/include/topmenus.inc | 446 +++ source/sourcemod/scripting/include/updater.inc | 97 + .../sourcemod/scripting/include/usermessages.inc | 270 ++ source/sourcemod/scripting/include/vector.inc | 179 ++ source/sourcemod/scripting/include/version.inc | 49 + .../sourcemod/scripting/include/version_auto.inc | 15 + .../sourcemod/scripting/momsurffix/baseplayer.sp | 189 ++ .../sourcemod/scripting/momsurffix/gamemovement.sp | 411 +++ source/sourcemod/scripting/momsurffix/gametrace.sp | 480 +++ source/sourcemod/scripting/momsurffix/utils.sp | 279 ++ source/sourcemod/scripting/quakesoundsv3.sp | 1191 ++++++++ source/sourcemod/scripting/setpos_setang.sp | 70 + source/sourcemod/scripting/sm_speedometer.sp | 585 ++++ source/sourcemod/scripting/spcomp.exe | Bin 0 -> 1149952 bytes source/sourcemod/scripting/spcomp64.exe | Bin 0 -> 1433600 bytes 317 files changed, 102614 insertions(+) create mode 100644 source/sourcemod/GPLv2.txt create mode 100644 source/sourcemod/GPLv3.txt create mode 100644 source/sourcemod/LICENSE.txt create mode 100644 source/sourcemod/scripting/bot2player_public.sp create mode 100644 source/sourcemod/scripting/compile.exe create mode 100644 source/sourcemod/scripting/css-kztimer.sp create mode 100644 source/sourcemod/scripting/distbugfix.sp create mode 100644 source/sourcemod/scripting/distbugfix/clientprefs.sp create mode 100644 source/sourcemod/scripting/game_manager.sp create mode 100644 source/sourcemod/scripting/gem_damage_report.sp create mode 100644 source/sourcemod/scripting/gokz-anticheat.sp create mode 100644 source/sourcemod/scripting/gokz-anticheat/api.sp create mode 100644 source/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp create mode 100644 source/sourcemod/scripting/gokz-anticheat/commands.sp create mode 100644 source/sourcemod/scripting/gokz-chat.sp create mode 100644 source/sourcemod/scripting/gokz-core.sp create mode 100644 source/sourcemod/scripting/gokz-core/commands.sp create mode 100644 source/sourcemod/scripting/gokz-core/demofix.sp create mode 100644 source/sourcemod/scripting/gokz-core/forwards.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/buttons.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/end.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/mapfile.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/prefix.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/starts.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/triggers.sp create mode 100644 source/sourcemod/scripting/gokz-core/map/zones.sp create mode 100644 source/sourcemod/scripting/gokz-core/menus/mode_menu.sp create mode 100644 source/sourcemod/scripting/gokz-core/menus/options_menu.sp create mode 100644 source/sourcemod/scripting/gokz-core/misc.sp create mode 100644 source/sourcemod/scripting/gokz-core/modes.sp create mode 100644 source/sourcemod/scripting/gokz-core/natives.sp create mode 100644 source/sourcemod/scripting/gokz-core/options.sp create mode 100644 source/sourcemod/scripting/gokz-core/teamnumfix.sp create mode 100644 source/sourcemod/scripting/gokz-core/teleports.sp create mode 100644 source/sourcemod/scripting/gokz-core/timer/pause.sp create mode 100644 source/sourcemod/scripting/gokz-core/timer/timer.sp create mode 100644 source/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp create mode 100644 source/sourcemod/scripting/gokz-core/triggerfix.sp create mode 100644 source/sourcemod/scripting/gokz-errorboxfixer.sp create mode 100644 source/sourcemod/scripting/gokz-global.sp create mode 100644 source/sourcemod/scripting/gokz-global/api.sp create mode 100644 source/sourcemod/scripting/gokz-global/ban_player.sp create mode 100644 source/sourcemod/scripting/gokz-global/commands.sp create mode 100644 source/sourcemod/scripting/gokz-global/maptop_menu.sp create mode 100644 source/sourcemod/scripting/gokz-global/points.sp create mode 100644 source/sourcemod/scripting/gokz-global/print_records.sp create mode 100644 source/sourcemod/scripting/gokz-global/send_run.sp create mode 100644 source/sourcemod/scripting/gokz-goto.sp create mode 100644 source/sourcemod/scripting/gokz-hud.sp create mode 100644 source/sourcemod/scripting/gokz-hud/commands.sp create mode 100644 source/sourcemod/scripting/gokz-hud/hide_weapon.sp create mode 100644 source/sourcemod/scripting/gokz-hud/info_panel.sp create mode 100644 source/sourcemod/scripting/gokz-hud/menu.sp create mode 100644 source/sourcemod/scripting/gokz-hud/natives.sp create mode 100644 source/sourcemod/scripting/gokz-hud/options.sp create mode 100644 source/sourcemod/scripting/gokz-hud/options_menu.sp create mode 100644 source/sourcemod/scripting/gokz-hud/racing_text.sp create mode 100644 source/sourcemod/scripting/gokz-hud/spectate_text.sp create mode 100644 source/sourcemod/scripting/gokz-hud/speed_text.sp create mode 100644 source/sourcemod/scripting/gokz-hud/timer_text.sp create mode 100644 source/sourcemod/scripting/gokz-hud/tp_menu.sp create mode 100644 source/sourcemod/scripting/gokz-jumpbeam.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/api.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/commands.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/jump_validating.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/options.sp create mode 100644 source/sourcemod/scripting/gokz-jumpstats/options_menu.sp create mode 100644 source/sourcemod/scripting/gokz-localdb.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/api.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/commands.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/cache_js.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/create_tables.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/helpers.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/save_js.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/save_time.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/set_cheater.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/setup_client.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/setup_database.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/setup_map.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/sql.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/db/timer_setup.sp create mode 100644 source/sourcemod/scripting/gokz-localdb/options.sp create mode 100644 source/sourcemod/scripting/gokz-localranks.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/api.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/commands.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/cache_records.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/create_tables.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/display_js.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/get_completion.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/helpers.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/js_top.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/map_top.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/player_top.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/print_average.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/print_js.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/print_pbs.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/print_records.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/process_new_time.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/recent_records.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/sql.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp create mode 100644 source/sourcemod/scripting/gokz-localranks/misc.sp create mode 100644 source/sourcemod/scripting/gokz-measure.sp create mode 100644 source/sourcemod/scripting/gokz-measure/commands.sp create mode 100644 source/sourcemod/scripting/gokz-measure/measure_menu.sp create mode 100644 source/sourcemod/scripting/gokz-measure/measurer.sp create mode 100644 source/sourcemod/scripting/gokz-mode-kztimer.sp create mode 100644 source/sourcemod/scripting/gokz-mode-simplekz.sp create mode 100644 source/sourcemod/scripting/gokz-mode-vanilla.sp create mode 100644 source/sourcemod/scripting/gokz-momsurffix.sp create mode 100644 source/sourcemod/scripting/gokz-paint.sp create mode 100644 source/sourcemod/scripting/gokz-pistol.sp create mode 100644 source/sourcemod/scripting/gokz-playermodels.sp create mode 100644 source/sourcemod/scripting/gokz-profile.sp create mode 100644 source/sourcemod/scripting/gokz-profile/options.sp create mode 100644 source/sourcemod/scripting/gokz-profile/profile.sp create mode 100644 source/sourcemod/scripting/gokz-quiet.sp create mode 100644 source/sourcemod/scripting/gokz-quiet/ambient.sp create mode 100644 source/sourcemod/scripting/gokz-quiet/falldamage.sp create mode 100644 source/sourcemod/scripting/gokz-quiet/gokz-sounds.sp create mode 100644 source/sourcemod/scripting/gokz-quiet/hideplayers.sp create mode 100644 source/sourcemod/scripting/gokz-quiet/options.sp create mode 100644 source/sourcemod/scripting/gokz-quiet/soundscape.sp create mode 100644 source/sourcemod/scripting/gokz-racing.sp create mode 100644 source/sourcemod/scripting/gokz-racing/announce.sp create mode 100644 source/sourcemod/scripting/gokz-racing/api.sp create mode 100644 source/sourcemod/scripting/gokz-racing/commands.sp create mode 100644 source/sourcemod/scripting/gokz-racing/duel_menu.sp create mode 100644 source/sourcemod/scripting/gokz-racing/race.sp create mode 100644 source/sourcemod/scripting/gokz-racing/race_menu.sp create mode 100644 source/sourcemod/scripting/gokz-racing/racer.sp create mode 100644 source/sourcemod/scripting/gokz-replays.sp create mode 100644 source/sourcemod/scripting/gokz-replays/api.sp create mode 100644 source/sourcemod/scripting/gokz-replays/commands.sp create mode 100644 source/sourcemod/scripting/gokz-replays/controls.sp create mode 100644 source/sourcemod/scripting/gokz-replays/nav.sp create mode 100644 source/sourcemod/scripting/gokz-replays/playback.sp create mode 100644 source/sourcemod/scripting/gokz-replays/recording.sp create mode 100644 source/sourcemod/scripting/gokz-replays/replay_cache.sp create mode 100644 source/sourcemod/scripting/gokz-replays/replay_menu.sp create mode 100644 source/sourcemod/scripting/gokz-saveloc.sp create mode 100644 source/sourcemod/scripting/gokz-slayonend.sp create mode 100644 source/sourcemod/scripting/gokz-spec.sp create mode 100644 source/sourcemod/scripting/gokz-tips.sp create mode 100644 source/sourcemod/scripting/gokz-tpanglefix.sp create mode 100644 source/sourcemod/scripting/include/GlobalAPI.inc create mode 100644 source/sourcemod/scripting/include/GlobalAPI/iterable.inc create mode 100644 source/sourcemod/scripting/include/GlobalAPI/request.inc create mode 100644 source/sourcemod/scripting/include/GlobalAPI/requestdata.inc create mode 100644 source/sourcemod/scripting/include/GlobalAPI/responses.inc create mode 100644 source/sourcemod/scripting/include/GlobalAPI/stocks.inc create mode 100644 source/sourcemod/scripting/include/SteamWorks.inc create mode 100644 source/sourcemod/scripting/include/admin.inc create mode 100644 source/sourcemod/scripting/include/adminmenu.inc create mode 100644 source/sourcemod/scripting/include/adt.inc create mode 100644 source/sourcemod/scripting/include/adt_array.inc create mode 100644 source/sourcemod/scripting/include/adt_stack.inc create mode 100644 source/sourcemod/scripting/include/adt_trie.inc create mode 100644 source/sourcemod/scripting/include/autoexecconfig.inc create mode 100644 source/sourcemod/scripting/include/banning.inc create mode 100644 source/sourcemod/scripting/include/basecomm.inc create mode 100644 source/sourcemod/scripting/include/bitbuffer.inc create mode 100644 source/sourcemod/scripting/include/clientprefs.inc create mode 100644 source/sourcemod/scripting/include/clients.inc create mode 100644 source/sourcemod/scripting/include/colors.inc create mode 100644 source/sourcemod/scripting/include/commandfilters.inc create mode 100644 source/sourcemod/scripting/include/commandline.inc create mode 100644 source/sourcemod/scripting/include/console.inc create mode 100644 source/sourcemod/scripting/include/convars.inc create mode 100644 source/sourcemod/scripting/include/core.inc create mode 100644 source/sourcemod/scripting/include/cstrike.inc create mode 100644 source/sourcemod/scripting/include/datapack.inc create mode 100644 source/sourcemod/scripting/include/dbi.inc create mode 100644 source/sourcemod/scripting/include/dhooks.inc create mode 100644 source/sourcemod/scripting/include/distbugfix.inc create mode 100644 source/sourcemod/scripting/include/entity.inc create mode 100644 source/sourcemod/scripting/include/entity_prop_stocks.inc create mode 100644 source/sourcemod/scripting/include/entitylump.inc create mode 100644 source/sourcemod/scripting/include/events.inc create mode 100644 source/sourcemod/scripting/include/files.inc create mode 100644 source/sourcemod/scripting/include/float.inc create mode 100644 source/sourcemod/scripting/include/functions.inc create mode 100644 source/sourcemod/scripting/include/gamechaos.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/arrays.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/client.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/debug.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/isvalidclient.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/maths.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/misc.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/strings.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/tempents.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/tracing.inc create mode 100644 source/sourcemod/scripting/include/gamechaos/vectors.inc create mode 100644 source/sourcemod/scripting/include/geoip.inc create mode 100644 source/sourcemod/scripting/include/glib/addressutils.inc create mode 100644 source/sourcemod/scripting/include/glib/assertutils.inc create mode 100644 source/sourcemod/scripting/include/glib/memutils.inc create mode 100644 source/sourcemod/scripting/include/gokz.inc create mode 100644 source/sourcemod/scripting/include/gokz/anticheat.inc create mode 100644 source/sourcemod/scripting/include/gokz/chat.inc create mode 100644 source/sourcemod/scripting/include/gokz/core.inc create mode 100644 source/sourcemod/scripting/include/gokz/global.inc create mode 100644 source/sourcemod/scripting/include/gokz/hud.inc create mode 100644 source/sourcemod/scripting/include/gokz/jumpbeam.inc create mode 100644 source/sourcemod/scripting/include/gokz/jumpstats.inc create mode 100644 source/sourcemod/scripting/include/gokz/kzplayer.inc create mode 100644 source/sourcemod/scripting/include/gokz/localdb.inc create mode 100644 source/sourcemod/scripting/include/gokz/localranks.inc create mode 100644 source/sourcemod/scripting/include/gokz/momsurffix.inc create mode 100644 source/sourcemod/scripting/include/gokz/paint.inc create mode 100644 source/sourcemod/scripting/include/gokz/pistol.inc create mode 100644 source/sourcemod/scripting/include/gokz/profile.inc create mode 100644 source/sourcemod/scripting/include/gokz/quiet.inc create mode 100644 source/sourcemod/scripting/include/gokz/racing.inc create mode 100644 source/sourcemod/scripting/include/gokz/replays.inc create mode 100644 source/sourcemod/scripting/include/gokz/slayonend.inc create mode 100644 source/sourcemod/scripting/include/gokz/tips.inc create mode 100644 source/sourcemod/scripting/include/gokz/tpanglefix.inc create mode 100644 source/sourcemod/scripting/include/gokz/version.inc create mode 100644 source/sourcemod/scripting/include/halflife.inc create mode 100644 source/sourcemod/scripting/include/handles.inc create mode 100644 source/sourcemod/scripting/include/helpers.inc create mode 100644 source/sourcemod/scripting/include/json.inc create mode 100644 source/sourcemod/scripting/include/json/decode_helpers.inc create mode 100644 source/sourcemod/scripting/include/json/definitions.inc create mode 100644 source/sourcemod/scripting/include/json/encode_helpers.inc create mode 100644 source/sourcemod/scripting/include/json/helpers/decode.inc create mode 100644 source/sourcemod/scripting/include/json/helpers/encode.inc create mode 100644 source/sourcemod/scripting/include/json/helpers/string.inc create mode 100644 source/sourcemod/scripting/include/json/object.inc create mode 100644 source/sourcemod/scripting/include/json/string_helpers.inc create mode 100644 source/sourcemod/scripting/include/keyvalues.inc create mode 100644 source/sourcemod/scripting/include/lang.inc create mode 100644 source/sourcemod/scripting/include/logging.inc create mode 100644 source/sourcemod/scripting/include/mapchooser.inc create mode 100644 source/sourcemod/scripting/include/menus.inc create mode 100644 source/sourcemod/scripting/include/morecolors.inc create mode 100644 source/sourcemod/scripting/include/movement.inc create mode 100644 source/sourcemod/scripting/include/movementapi.inc create mode 100644 source/sourcemod/scripting/include/nextmap.inc create mode 100644 source/sourcemod/scripting/include/profiler.inc create mode 100644 source/sourcemod/scripting/include/protobuf.inc create mode 100644 source/sourcemod/scripting/include/regex.inc create mode 100644 source/sourcemod/scripting/include/sdkhooks.inc create mode 100644 source/sourcemod/scripting/include/sdktools.inc create mode 100644 source/sourcemod/scripting/include/sdktools_client.inc create mode 100644 source/sourcemod/scripting/include/sdktools_engine.inc create mode 100644 source/sourcemod/scripting/include/sdktools_entinput.inc create mode 100644 source/sourcemod/scripting/include/sdktools_entoutput.inc create mode 100644 source/sourcemod/scripting/include/sdktools_functions.inc create mode 100644 source/sourcemod/scripting/include/sdktools_gamerules.inc create mode 100644 source/sourcemod/scripting/include/sdktools_hooks.inc create mode 100644 source/sourcemod/scripting/include/sdktools_sound.inc create mode 100644 source/sourcemod/scripting/include/sdktools_stocks.inc create mode 100644 source/sourcemod/scripting/include/sdktools_stringtables.inc create mode 100644 source/sourcemod/scripting/include/sdktools_tempents.inc create mode 100644 source/sourcemod/scripting/include/sdktools_tempents_stocks.inc create mode 100644 source/sourcemod/scripting/include/sdktools_trace.inc create mode 100644 source/sourcemod/scripting/include/sdktools_variant_t.inc create mode 100644 source/sourcemod/scripting/include/sdktools_voice.inc create mode 100644 source/sourcemod/scripting/include/smjansson.inc create mode 100644 source/sourcemod/scripting/include/smlib.inc create mode 100644 source/sourcemod/scripting/include/smlib/arrays.inc create mode 100644 source/sourcemod/scripting/include/smlib/clients.inc create mode 100644 source/sourcemod/scripting/include/smlib/colors.inc create mode 100644 source/sourcemod/scripting/include/smlib/concommands.inc create mode 100644 source/sourcemod/scripting/include/smlib/convars.inc create mode 100644 source/sourcemod/scripting/include/smlib/crypt.inc create mode 100644 source/sourcemod/scripting/include/smlib/debug.inc create mode 100644 source/sourcemod/scripting/include/smlib/dynarrays.inc create mode 100644 source/sourcemod/scripting/include/smlib/edicts.inc create mode 100644 source/sourcemod/scripting/include/smlib/effects.inc create mode 100644 source/sourcemod/scripting/include/smlib/entities.inc create mode 100644 source/sourcemod/scripting/include/smlib/files.inc create mode 100644 source/sourcemod/scripting/include/smlib/game.inc create mode 100644 source/sourcemod/scripting/include/smlib/general.inc create mode 100644 source/sourcemod/scripting/include/smlib/math.inc create mode 100644 source/sourcemod/scripting/include/smlib/menus.inc create mode 100644 source/sourcemod/scripting/include/smlib/server.inc create mode 100644 source/sourcemod/scripting/include/smlib/sql.inc create mode 100644 source/sourcemod/scripting/include/smlib/strings.inc create mode 100644 source/sourcemod/scripting/include/smlib/teams.inc create mode 100644 source/sourcemod/scripting/include/smlib/vehicles.inc create mode 100644 source/sourcemod/scripting/include/smlib/weapons.inc create mode 100644 source/sourcemod/scripting/include/smlib/world.inc create mode 100644 source/sourcemod/scripting/include/sorting.inc create mode 100644 source/sourcemod/scripting/include/sourcebanspp.inc create mode 100644 source/sourcemod/scripting/include/sourcemod-colors.inc create mode 100644 source/sourcemod/scripting/include/sourcemod.inc create mode 100644 source/sourcemod/scripting/include/string.inc create mode 100644 source/sourcemod/scripting/include/testing.inc create mode 100644 source/sourcemod/scripting/include/textparse.inc create mode 100644 source/sourcemod/scripting/include/tf2.inc create mode 100644 source/sourcemod/scripting/include/tf2_stocks.inc create mode 100644 source/sourcemod/scripting/include/timers.inc create mode 100644 source/sourcemod/scripting/include/topmenus.inc create mode 100644 source/sourcemod/scripting/include/updater.inc create mode 100644 source/sourcemod/scripting/include/usermessages.inc create mode 100644 source/sourcemod/scripting/include/vector.inc create mode 100644 source/sourcemod/scripting/include/version.inc create mode 100644 source/sourcemod/scripting/include/version_auto.inc create mode 100644 source/sourcemod/scripting/momsurffix/baseplayer.sp create mode 100644 source/sourcemod/scripting/momsurffix/gamemovement.sp create mode 100644 source/sourcemod/scripting/momsurffix/gametrace.sp create mode 100644 source/sourcemod/scripting/momsurffix/utils.sp create mode 100644 source/sourcemod/scripting/quakesoundsv3.sp create mode 100644 source/sourcemod/scripting/setpos_setang.sp create mode 100644 source/sourcemod/scripting/sm_speedometer.sp create mode 100644 source/sourcemod/scripting/spcomp.exe create mode 100644 source/sourcemod/scripting/spcomp64.exe (limited to 'source/sourcemod') diff --git a/source/sourcemod/GPLv2.txt b/source/sourcemod/GPLv2.txt new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/source/sourcemod/GPLv2.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/source/sourcemod/GPLv3.txt b/source/sourcemod/GPLv3.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/source/sourcemod/GPLv3.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/source/sourcemod/LICENSE.txt b/source/sourcemod/LICENSE.txt new file mode 100644 index 0000000..67bb9ed --- /dev/null +++ b/source/sourcemod/LICENSE.txt @@ -0,0 +1,36 @@ +SOURCEMOD LICENSE INFORMATION +VERSION: 2019-9-30 +----------------------------- + +SourceMod is licensed under the GNU General Public License, version 3. + +As a special exception, AlliedModders LLC gives you permission to link the code +of this program (as well as its derivative works) to "Half-Life 2," the "Source +Engine" and any Game MODs that run on software by the Valve Corporation. +You must obey the GNU General Public License in all respects for all other code used. +Additionally, AlliedModders LLC grants this exception to all derivative works. + +As an additional special exception to the GNU General Public License 3.0, +AlliedModders LLC permits dual-licensing of DERIVATIVE WORKS ONLY (that is, +SourcePawn/SourceMod Plugins and SourceMod Extensions, or any software built +from the SourceMod SDK or header files) under the GNU General Public License +version 2 "or any higher version." As such, you may choose for your derivative +work(s) to be compatible with the GNU General Public License version 2 as long +as it is also compatible with the GNU General Public License version 3, via the +"or any higher version" clause. This is intended for compatibility with other +software. + +As a final exception to the above, any derivative works created prior to this +date (July 31, 2007) may be exclusively licensed under the GNU General Public +License version 2 (without an "or any higher version" clause) if and only if +the work was already GNU General Public License 2.0 exclusive. This clause is +provided for backwards compatibility only. + +A copy of the GNU General Public License 2.0 is available in GPLv2.txt. +A copy of the GNU General Public License 3.0 is available in GPLv3.txt. + +SourcePawn is Copyright (C) 2006-2008 AlliedModders LLC. All rights reserved. +SourceMod is Copyright (C) 2006-2008 AlliedModders LLC. All rights reserved. +Pawn and SMALL are Copyright (C) 1997-2008 ITB CompuPhase. +Source is Copyright (C) Valve Corporation. +All trademarks are property of their respective owners in the US and other countries. diff --git a/source/sourcemod/scripting/bot2player_public.sp b/source/sourcemod/scripting/bot2player_public.sp new file mode 100644 index 0000000..a295d92 --- /dev/null +++ b/source/sourcemod/scripting/bot2player_public.sp @@ -0,0 +1,527 @@ +#include +#include +#include +#include +#include + +new const String:PLUGIN_NAME[]= "CS:S Bot2Player (public)" +new const String: PLUGIN_AUTHOR[]= "Bittersweet" +new const String:PLUGIN_DESCRIPTION[]= "Allows players to control bots after they've died (adapted from botcontrol for TF2 by Grognak)" +new const String: PLUGIN_VERSION[]= "public.2013.06.23.23.46" +new Handle:cvar_b2p_enabled = INVALID_HANDLE +new Handle:cvar_Round_Restart_Delay = INVALID_HANDLE +new Handle:cvar_BotTakeOverStartingCost = INVALID_HANDLE +new Handle:cvar_BotTakeOverCostIncrement = INVALID_HANDLE + +//These are default values, actually set from bot2player_public.cfg file +new BotTakeOverStartingCost = 1000 +new BotTakeOverCostIncrement = 250 + +new bool:bHideDeath[MAXPLAYERS + 1] = {false, ...}; +new ClientSpecClient[MAXPLAYERS + 1] = {0, ...} +new ClientTookover[MAXPLAYERS + 1] = {0, ...} +new WrongTeamWarning[MAXPLAYERS + 1] = {0, ...} +new TeleportWarning[MAXPLAYERS + 1] = {0, ...} +new BotTakeverCost[MAXPLAYERS + 1] = {0, ...} +new NeedsDuck[MAXPLAYERS + 1] = {0, ...} +new Nades[MAXPLAYERS + 1][3] +new CanUseWeapons[MAXPLAYERS + 1] = {true, ...}; + +new Float:Round_Restart_Delay = 0.0 +new Float:Weapon_Strip_Delay = 0.0 +new bool:b2pEnabled +new iTargetActiveWeapon +new g_offObserverTarget +new iTargetWeapon[5] +new iTargetClip[5] +new iTargetAmmo[5] +new g_iAccount = -1 +new gameround = 1 + +new String:iTargetActiveWeaponName[32] + +public Plugin:myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, +} +public OnPluginStart() +{ + PrintToServer("[%s %s] - Loaded", PLUGIN_NAME, PLUGIN_VERSION) + HookEvent("player_death", Event_PlayerDeath, EventHookMode_Pre) + HookEvent("round_start", Event_RoundStart) + HookEvent("round_end", Event_RoundEnd, EventHookMode_Pre) + // round_freeze_end only used for debugging + //HookEvent("round_freeze_end", Event_RoundFreezeEnd) + g_iAccount = FindSendPropOffs("CCSPlayer", "m_iAccount"); + g_offObserverTarget = FindSendPropOffs("CBasePlayer", "m_hObserverTarget") + if(g_offObserverTarget == -1) + { + SetFailState("Expected to find the offset to m_hObserverTarget, couldn't.") + } + AddCommandListener(NewTarget, "spec_next") + AddCommandListener(NewTarget, "spec_prev") + CreateConVar("bot2player_version", PLUGIN_VERSION, "Bot2player (public) version", FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_PLUGIN|FCVAR_SPONLY|FCVAR_DONTRECORD) + cvar_b2p_enabled = CreateConVar("bot2player_enabled", "1", "Enable the plugin?", FCVAR_PLUGIN, true, 0.0, true, 1.0) + cvar_BotTakeOverStartingCost = CreateConVar("bot2player_price", "1000", "Starting cost to take over a BOT (resets each map)", FCVAR_PLUGIN) + cvar_BotTakeOverCostIncrement = CreateConVar("bot2player_increase", "250", "Amount to raise price each time a player takes over a BOT", FCVAR_PLUGIN) + if (cvar_b2p_enabled == INVALID_HANDLE) + { + new String:FailReason[256] + Format(FailReason, sizeof(FailReason), "[%s] - cvar_b2p_enabled returned INVALID_HANDLE", PLUGIN_NAME) + EpicFail(FailReason) + } + cvar_Round_Restart_Delay = FindConVar("mp_round_restart_delay") + if (cvar_Round_Restart_Delay == INVALID_HANDLE) + { + new String:FailReason[256] + Format(FailReason, sizeof(FailReason), "[%s] - cvar_b2p_enabled returned INVALID_HANDLE", PLUGIN_NAME) + EpicFail(FailReason) + } + AutoExecConfig(true, "bot2player_public") +} +public OnConfigsExecuted() +{ + //based on 5.0 second mp_round_restart_delay: 4.6 just a hair early, 5.0 too late - 4.7 seems good - use mp_round_restart_delay - 0.3 + Round_Restart_Delay = GetConVarFloat(cvar_Round_Restart_Delay) + Weapon_Strip_Delay = Round_Restart_Delay - 0.3 + b2pEnabled = GetConVarBool(cvar_b2p_enabled) + BotTakeOverStartingCost = GetConVarInt(cvar_BotTakeOverStartingCost) + BotTakeOverCostIncrement = GetConVarInt(cvar_BotTakeOverCostIncrement) + HookConVarChange(cvar_b2p_enabled, cvar_b2p_enabledChange) + HookConVarChange(cvar_Round_Restart_Delay, cvar_b2p_enabledChange) + HookConVarChange(cvar_BotTakeOverStartingCost, CvarStartCostChange) + HookConVarChange(cvar_BotTakeOverCostIncrement, CvarCostIncreaseChange) +} +public OnMapStart() +{ + for (new i = 1; i <= MAXPLAYERS; i++) + { + BotTakeverCost[i] = BotTakeOverStartingCost + TeleportWarning[i] = 0 + } + gameround = 1 +} +public Action:Event_RoundStart(Handle:Event, const String:name[], bool:dontBroadcast) +{ + for (new i = 1; i <= MaxClients; i++) + { + ClientSpecClient[i] = 0 + WrongTeamWarning[i] = 0 + NeedsDuck[i] = 0 + CanUseWeapons[i] = true; + + if (IsClientConnected(i) && IsClientInGame(i) && !IsClientObserver(i) && ClientTookover[i]) + { + CreateTimer(0.25, StripWeapons, GetClientUserId(i)); + } + ClientTookover[i] = 0 + } +} + + +public Action:Event_RoundEnd(Handle:Event, const String:name[], bool:dontBroadcast) +{ + for (new i = 1; i <= MaxClients; i++) + { + ClientSpecClient[i] = 0 + WrongTeamWarning[i] = 0 + } + gameround++ +} +public Action:Event_RoundFreezeEnd(Handle:Event, const String:name[], bool:dontBroadcast) +{ + //This entire routine is for debugging only + PrintToServer("Round %i -----------------------------------------------------------------------", gameround) + for (new i = 1; i <= MaxClients; i++) + { + if(!IsClientConnected(i) || !IsClientInGame(i) || IsClientObserver(i)) continue + new TempWeapon[5] + for (new ii = 0; ii <= 4; ii++) + { + TempWeapon[ii] = Client_GetWeaponBySlot(i, ii) + if (TempWeapon[ii] > -1) + { + new String:Weapon[32] + GetEdictClassname(TempWeapon[ii], Weapon, sizeof(Weapon)) + if (ii == 3) + { + new tnades = GetAllClientGrenades(i) + if (tnades) + { + PrintToServer("Nade report for %N:", i) + if (Nades[i][0]) PrintToServer("%i HE Nades", Nades[i][0]) + if (Nades[i][1]) PrintToServer("%i Flash Nades", Nades[i][1]) + if (Nades[i][2]) PrintToServer("%i Smoke Nades", Nades[i][2]) + PrintToServer ("Line 114 - Total = %i", tnades) + } + } + } + } + } +} +public Action:OnPlayerRunCmd(iClient, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon) +{ + if( !b2pEnabled ) + return Plugin_Continue; + + if( IsPlayerAlive(iClient) ) { + if( NeedsDuck[iClient] > 0 ) { + buttons |= IN_DUCK; + --NeedsDuck[iClient]; + } + + return Plugin_Continue; + } + + if (!IsClientConnected(iClient) || !(buttons & IN_USE) || !IsClientObserver(iClient)) return Plugin_Continue + new iTarget = GetEntPropEnt(iClient, Prop_Send, "m_hObserverTarget") + new ClientCash = GetMoney(iClient) + if (IsValidClient(iTarget) && IsFakeClient(iTarget) && GetClientTeam(iClient) == GetClientTeam(iTarget) && ClientCash >= BotTakeverCost[iClient]) + { + //Get all of BOTs stats + new Float:iTargetOrigin[3], Float:iTargetAngles[3] + GetClientAbsOrigin(iTarget, iTargetOrigin) + GetClientAbsAngles(iTarget, iTargetAngles) + new iTargetHealth = GetClientHealth(iTarget) + new iTargetArmor = GetClientArmor(iTarget) + iTargetActiveWeapon = Client_GetActiveWeapon(iTarget) + for (new i = 0; i <= 1; i++) + { + iTargetWeapon[i] = Client_GetWeaponBySlot(iTarget, i) + if (iTargetWeapon[i] > -1) + { + Client_SetActiveWeapon(iTarget, iTargetWeapon[i]) + Client_GetActiveWeaponName(iTarget, iTargetActiveWeaponName, sizeof(iTargetActiveWeaponName)) + iTargetClip[i] = Weapon_GetPrimaryClip(iTargetWeapon[i]) + Client_GetWeaponPlayerAmmo(iTarget, iTargetActiveWeaponName, iTargetAmmo[i]) + + CS_DropWeapon(iTarget, GetPlayerWeaponSlot(iTarget, i), false, false); + } + else + { + iTargetClip[i] = 0 + iTargetAmmo[i] = 0 + } + } + //Check if target is alive one last time - fix for Known issue# 2 + if (!IsClientConnected(iTarget) || !IsValidClient(iTarget) || !IsPlayerAlive(iTarget)) + { + PrintHintText(iClient, "The BOT you tried to take over is no longer available for take over") + return Plugin_Continue + } + //Set all of humans stats, but not weapons + SetEntityHealth(iClient, iTargetHealth) + Client_SetArmor(iClient, iTargetArmor) + GetAllClientGrenades(iTarget) + CreateTimer(0.05, Give_iTargetWeaponsTo_iClient, iClient) + //Take control + bHideDeath[iTarget] = true + ClientTookover[iClient] = 1 + ClientSpecClient[iClient] = 0 + ClientCash = ClientCash - BotTakeverCost[iClient] + SetMoney(iClient, ClientCash) + BotTakeverCost[iClient] = BotTakeverCost[iClient] + BotTakeOverCostIncrement + //check for last player on team alive + new MyTeam = GetClientTeam(iClient) + new TeamMatesAlive = 0 + for (new i = 1; i <= MaxClients; i++) + { + if (!IsClientConnected(i) || i == iClient) continue + if (IsClientInGame(iClient) && IsClientInGame(i) && IsPlayerAlive(i) && MyTeam == GetClientTeam(i)) + { + TeamMatesAlive++ + } + } + new Handle:NoEndRoundHandle = FindConVar("mp_ignore_round_win_conditions") + if (TeamMatesAlive == 1) + { + SetConVarInt(NoEndRoundHandle, 1) + } + ForcePlayerSuicide(iTarget) + + // note(cristeigabriel): seems to fix getting stuck while + // in position that requires crouching + int ducked, ducking; + float ducktime2, duckjumptime; + int injumpduck; + + ducked = GetEntProp(iTarget, Prop_Send, "m_bDucked", 1); + ducking = GetEntProp(iTarget, Prop_Send, "m_bDucking", 1); + ducktime2 = GetEntPropFloat(iTarget, Prop_Send, "m_flDucktime"); + duckjumptime = GetEntPropFloat(iTarget, Prop_Send, "m_flDuckJumpTime"); + injumpduck = GetEntProp(iTarget, Prop_Send, "m_bInDuckJump", 1); + + CS_RespawnPlayer(iClient) + Client_RemoveAllWeapons(iClient) + Client_GiveWeapon(iClient, "weapon_knife", true) + CanUseWeapons[iClient] = false; + CreateTimer(0.1, AllowUse, GetClientUserId(iClient)); + + SetEntProp(iClient, Prop_Send, "m_bDucked", ducked, 1); + SetEntProp(iClient, Prop_Send, "m_bDucking", ducking, 1); + SetEntPropFloat(iClient, Prop_Send, "m_flDucktime", ducktime2); + SetEntPropFloat(iClient, Prop_Send, "m_flDuckJumpTime", duckjumptime); + SetEntProp(iClient, Prop_Send, "m_bInDuckJump", injumpduck, 1); + + // duck for 10 ticks after respawn. fixes moving at full speed while ducked. + if( ducked || ducktime2 != 0 ) { + NeedsDuck[iClient] = 10; + buttons |= IN_DUCK + } + + TeleportEntity(iClient, iTargetOrigin, iTargetAngles, NULL_VECTOR) + SetConVarInt(NoEndRoundHandle, 0) + PrintToChatAll("%N took control of %N", iClient, iTarget) + return Plugin_Handled + } + return Plugin_Continue +} +public GetAllClientGrenades(client) +{ + Nades[client][0] = 0 + Nades[client][1] = 0 + Nades[client][2] = 0 + new offsNades = FindDataMapOffs(client, "m_iAmmo") + (11 * 4); + new granadesnr = GetEntData(client, offsNades) + new lastgranadesnr = 0 + if (granadesnr > lastgranadesnr) + { + // HE Nades + Nades[client][0] = granadesnr + lastgranadesnr = granadesnr + } + offsNades += 4 + granadesnr += GetEntData(client, offsNades) + if (granadesnr > lastgranadesnr) + { + // Flashbangs + Nades[client][1] = granadesnr - lastgranadesnr + lastgranadesnr = granadesnr + } + offsNades += 4 + granadesnr += GetEntData(client, offsNades) + if (granadesnr > lastgranadesnr) + { + // Smoke Nades + Nades[client][2] = granadesnr - lastgranadesnr + lastgranadesnr = granadesnr + } + return granadesnr +} +public OnClientPostAdminCheck(client) +{ + BotTakeverCost[client] = BotTakeOverStartingCost + ClientSpecClient[client] = 0 + ClientTookover[client] = 0 + WrongTeamWarning[client] = 0 + SDKHook(client, SDKHook_WeaponCanUse, SDKWeaponCanUse); +} + +public Action:StripWeapons(Handle:timer, any:UserID) +{ + new client = GetClientOfUserId(UserID) + if (!client || !IsClientConnected(client)) return + Client_RemoveAllWeapons(client) + Client_GiveWeapon(client, "weapon_knife", true) + new playerTeam = GetClientTeam( client ); + if( playerTeam == CS_TEAM_T ) { + GivePlayerItem( client, "weapon_glock" ); + } + else if( playerTeam == CS_TEAM_CT ) { + GivePlayerItem( client, "weapon_usp" ); + } +} + +public Action:AllowUse(Handle:timer, any:userID) { + new client = GetClientOfUserId(userID); + if (!client || !IsClientConnected(client)) return + CanUseWeapons[client] = true; +} + +public Action:SDKWeaponCanUse(int client, int weapon) { + if(!CanUseWeapons[client]) + return Plugin_Handled; + + return Plugin_Continue; +} + +public Action:NewTarget(iClient, const String:cmd[], args) +{ + new iTarget = GetEntPropEnt(iClient, Prop_Send, "m_hObserverTarget") + if (!b2pEnabled || !IsValidClient(iTarget) || !IsClientObserver(iClient)) return Plugin_Continue; + CreateTimer(0.1, DisplayTakeOverMessage, iClient) + return Plugin_Continue; +} +public Action:DisplayTakeOverMessage(Handle:timer, any:iClient) +{ + if (!b2pEnabled || !IsClientConnected(iClient)) return Plugin_Continue + new ClientTeam = 0 + if (IsClientConnected(iClient)) + { + ClientTeam = GetClientTeam(iClient) + } + if (ClientTeam < 2) return Plugin_Continue + new iTarget = -1 + iTarget = GetEntPropEnt(iClient, Prop_Send, "m_hObserverTarget") + if (iTarget == -1) return Plugin_Continue + decl String:BOTName[64] + GetClientName(iTarget, BOTName, sizeof(BOTName)) + if (!IsValidClient(iTarget) || !IsClientObserver(iClient)) return Plugin_Continue + ClientSpecClient[iClient] = iTarget + new ClientCash = GetMoney(iClient) + if (ClientCash >= BotTakeverCost[iClient]) + { + if (IsFakeClient(iTarget)) + { + if (ClientTeam == GetClientTeam(iTarget)) + { + if (ClientCash >= BotTakeverCost[iClient]) + { + PrintHintText(iClient, "Press the Use key [default E] to take control of %s", BOTName) + return Plugin_Continue + } + else + { + PrintHintText(iClient, "You need $%i to take over any BOTs (the price increases each time you do)", BotTakeverCost[iClient]) + return Plugin_Continue + } + } + else + { + PrintHintText(iClient, "You can't take over BOTs that aren't on your team") + return Plugin_Continue + } + } + else + { + PrintHintText(iClient, "Spectate a BOT if you want to take over a BOT") + } + } + else + { + PrintHintText(iClient, "You need $%i to take over any BOTs (the price increases each time you do)", BotTakeverCost[iClient]) + return Plugin_Continue + } + return Plugin_Continue +} +public Action:Event_PlayerDeath(Handle:Event, const String:name[], bool:dontBroadcast) +{ + if (!b2pEnabled) return Plugin_Continue + new iClient = GetClientOfUserId(GetEventInt(Event, "userid")) + if (!IsClientConnected(iClient)) return Plugin_Continue + if (!IsFakeClient(iClient)) { + CreateTimer(6.75, DisplayTakeOverMessage, iClient) + return Plugin_Continue; + } + for (new i = 1; i <= MaxClients; i++) + { + new ClientCash = GetMoney(i) + if (IsClientConnected(iClient) && IsClientConnected(i) && IsClientInGame(i) && IsClientObserver(i) && ClientSpecClient[i] == iClient && ClientCash >= BotTakeverCost[i]) + { + PrintHintText(i, "%N died - You can't control dead BOTs", iClient) + ClientSpecClient[i] = 0 + } + } + if (!bHideDeath[iClient]) return Plugin_Continue + CreateTimer(0.2, tDestroyRagdoll, iClient) + return Plugin_Handled // Disable the killfeed notification for takeovers +} +public Action:tDestroyRagdoll(Handle:timer, any:iClient) +{ + new iRagdoll = GetEntPropEnt(iClient, Prop_Send, "m_hRagdoll") + bHideDeath[iClient] = false + if (iRagdoll < 0) return + AcceptEntityInput(iRagdoll, "kill"); +} +public Action:Give_iTargetWeaponsTo_iClient(Handle:timer, any:iClient) +{ + for (new i = 0; i <= 1; i++) + { + if (iTargetWeapon[i] != INVALID_ENT_REFERENCE) + { + Client_EquipWeapon(iClient, iTargetWeapon[i], false) + Client_SetActiveWeapon(iClient, iTargetWeapon[i]) + Client_GetActiveWeaponName(iClient, iTargetActiveWeaponName, sizeof(iTargetActiveWeaponName)) + Client_SetWeaponClipAmmo(iClient, iTargetActiveWeaponName, iTargetClip[i]) + Client_SetWeaponPlayerAmmo(iClient, iTargetActiveWeaponName, iTargetAmmo[i]) + } + Client_SetActiveWeapon(iClient, iTargetActiveWeapon) + } + if (Nades[iClient][0] > 0) GivePlayerItem(iClient, "weapon_hegrenade") + if (Nades[iClient][1] > 0) GivePlayerItem(iClient, "weapon_flashbang") + if (Nades[iClient][1] > 1) GivePlayerItem(iClient, "weapon_flashbang") + if (Nades[iClient][2] > 0) GivePlayerItem(iClient, "weapon_smokegrenade") +} +public cvar_b2p_enabledChange(Handle:convar, const String:oldValue[], const String:newValue[]) +{ + b2pEnabled = GetConVarBool(cvar_b2p_enabled) +} +public CvarRoundRestartDelayChange(Handle:convar, const String:oldValue[], const String:newValue[]) +{ + Round_Restart_Delay = GetConVarFloat(cvar_Round_Restart_Delay) + Weapon_Strip_Delay = Round_Restart_Delay - 0.3 +} +public CvarStartCostChange(Handle:convar, const String:oldValue[], const String:newValue[]) +{ + BotTakeOverStartingCost = GetConVarInt(cvar_BotTakeOverStartingCost) +} +public CvarCostIncreaseChange(Handle:convar, const String:oldValue[], const String:newValue[]) +{ + BotTakeOverCostIncrement = GetConVarInt(cvar_BotTakeOverCostIncrement) +} +stock FindRagdollClosestToEntity(iEntity, Float:fLimit) +{ + new iSearch = -1, + iReturn = -1; + new Float:fLowest = -1.0, + Float:fVectorDist, + Float:fEntityPos[3], + Float:fRagdollPos[3] + if (!IsValidEntity(iEntity)) return iReturn; + GetEntPropVector(iEntity, Prop_Send, "m_vecOrigin", fEntityPos); + while ((iSearch = FindEntityByClassname(iSearch, "tf_ragdoll")) != -1) + { + GetEntPropVector(iSearch, Prop_Send, "m_vecRagdollOrigin", fRagdollPos); + fVectorDist = GetVectorDistance(fEntityPos, fRagdollPos); + if (fVectorDist < fLimit && (fVectorDist < fLowest || fLowest == -1.0)) + { + fLowest = fVectorDist + iReturn = iSearch + } + } + return iReturn +} +stock bool:IsValidClient(iClient) +{ + if (iClient <= 0 || iClient > MaxClients || !IsClientInGame(iClient)) return false + return true +} +public GetMoney(client) +{ + if (!IsClientConnected(client) || !IsClientInGame(client)) return 0 + if (g_iAccount != -1) + { + return GetEntData(client, g_iAccount) + } + else + { + return 0 + } +} +public SetMoney(client, amount) +{ + if (!IsClientConnected(client) || !IsClientInGame(client)) return + if (g_iAccount != -1) + { + SetEntData(client, g_iAccount, amount) + } +} +public EpicFail(String:FailReason[]) +{ + PrintToServer("[%s] - Fatal Error: %s", PLUGIN_NAME, FailReason) + SetFailState("[%s] - Fatal Error: %s", PLUGIN_NAME, FailReason) +} +//End of diff --git a/source/sourcemod/scripting/compile.exe b/source/sourcemod/scripting/compile.exe new file mode 100644 index 0000000..5cfa717 Binary files /dev/null and b/source/sourcemod/scripting/compile.exe differ diff --git a/source/sourcemod/scripting/css-kztimer.sp b/source/sourcemod/scripting/css-kztimer.sp new file mode 100644 index 0000000..5070985 --- /dev/null +++ b/source/sourcemod/scripting/css-kztimer.sp @@ -0,0 +1,984 @@ +#include +#include +#include +#include +#include + +#define MAX_TIMES 8192 + +public Plugin myinfo = { + name = "KZTimer", + author = "networkheaven.net", + description = "KZTimer", + version = "420.69", + url = "networkheaven.net" +}; + +enum MapZoneType { + ZONE_NONE, + ZONE_BUTTON, + ZONE_TRIGGER, +}; + +enum struct PlayerData { + bool bIsInRun; + bool bIsInZone; + + float fStartTime; + float fEndTime; + float fPausedTime; + + int nJumps; + int nTeleports; + + bool bShowingMenu; + bool bShowViewmodel; + + float vStartPoint[3]; + float vStartAngle[3]; + + float vPausedAngle[3]; + float vLastAngle[3]; + + float vSavedPoint[3]; + float vSavedAngles[3]; + bool bSavedDuck; + bool bSavedPoint; + bool bPausedRun; + + int nDuckTicks; +} + +enum struct TimeData { + float fFinalTime; + int nJumps; + int nTeleports; + char sName[64]; + char sSteamid[32]; + char sMapName[32]; + int nTimestamp; + int nPosition; +} + +public bool g_isKZ; +public PlayerData g_playerData[MAXPLAYERS + 1]; +public Handle g_DB = INVALID_HANDLE; + +public int g_topTime = 0; +public TimeData g_times[MAX_TIMES]; +public Cookie g_hideWeaponCookie; + +public ConVar g_nhWarmup = null; + +public void OnPluginStart() { + HookEntityOutput( "func_button", "OnPressed", OnButtonUsed ); + + RegConsoleCmd( "sm_savepoint", Command_SavePoint, "Save your current position." ); + RegConsoleCmd( "sm_loadpoint", Command_LoadPoint, "Load your saved position." ); + RegConsoleCmd( "sm_tpmenu", Command_CheckpointPanel, "Show the checkpoint menu." ); + RegConsoleCmd( "sm_tp", Command_CheckpointPanel, "Show the checkpoint menu." ); + RegConsoleCmd( "sm_pause", Command_PauseRun, "Pause/resume your run." ); + RegConsoleCmd( "sm_restart", Command_Restart, "Restart your run." ); + RegConsoleCmd( "sm_maptop", Command_Maptop, "Show the top 50 times." ); + RegConsoleCmd( "sm_m", Command_Maptop, "Show the top 50 times." ); + RegConsoleCmd( "sm_mrank", Command_MyRank, "Show your rank on the current map." ); + RegConsoleCmd( "sm_viewmodel", Command_HideViewmodel, "Toggle viewmodel." ); + RegConsoleCmd( "sm_vm", Command_HideViewmodel, "Toggle viewmodel." ); + RegConsoleCmd( "sm_hideweapon", Command_HideViewmodel, "Toggle viewmodel." ); + RegConsoleCmd( "sm_noclip", Command_Noclip, "Toggle noclip." ); + + HookEvent( "player_spawn", Event_PlayerSpawn, EventHookMode_Post ); + HookEvent( "player_jump", Event_PlayerJump, EventHookMode_Post ); + + g_hideWeaponCookie = RegClientCookie( "kztimer_hideweapon", "kztimer_hideweapon", CookieAccess_Public ); +} + +public void OnPluginEnd() { +} + +public void OnAllPluginsLoaded() { + +} + +public void ClearPlayerData( int i ) { + g_playerData[i].bIsInRun = false; + g_playerData[i].bIsInZone = false; + g_playerData[i].fStartTime = 0.0; + g_playerData[i].fEndTime = 0.0; + g_playerData[i].fPausedTime = 0.0; + g_playerData[i].nJumps = 0; + g_playerData[i].nTeleports = 0; + g_playerData[i].bSavedPoint = false; + g_playerData[i].bPausedRun = false; + g_playerData[i].bShowingMenu = false; + g_playerData[i].nDuckTicks = 0; + g_playerData[i].bShowViewmodel = true; +} + +public void ClearAllPlayers() { + for( int i = 0; i < MAXPLAYERS; i++ ) { + ClearPlayerData( i ); + } +} + +public void OnClientPutInServer( int client ) { + ClearPlayerData( client ); + + SDKHook( client, SDKHook_OnTakeDamage, OnTakeDamage ); +} + +public void CreateDatabaseCallback( Handle owner, DBResultSet hndl, const char[] error, any pack ) { + if( hndl == INVALID_HANDLE ) { + LogError( "Failed to create database: %s", error ); + return; + } + + char mapName[256]; + GetCurrentMap( mapName, sizeof(mapName) ); + + LoadDatabase(); +} + +public void CreateDatabase() { + if( g_DB != INVALID_HANDLE ) { + CloseHandle( g_DB ); + } + + char error[256]; + g_DB = SQL_Connect( "kztimer", true, error, sizeof(error) ); + + if( g_DB == INVALID_HANDLE ) { + LogError( "Failed to connect to database: %s", error ); + return; + } + + SQL_TQuery( g_DB, CreateDatabaseCallback, "CREATE TABLE IF NOT EXISTS times ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, steamid VARCHAR(32) NOT NULL , name VARCHAR(64) NOT NULL , map VARCHAR(32) NOT NULL , time FLOAT NOT NULL , jumps INT NOT NULL , teleports INT NOT NULL , date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )" ); +} + +public void LoadDatabaseCallback( Handle owner, DBResultSet hndl, const char[] error, any pack ) { + if( hndl == INVALID_HANDLE ) { + LogError( "Failed to load database: %s", error ); + return; + } + + g_topTime = SQL_GetRowCount( hndl ); + LogMessage( "Loaded %d times", g_topTime ); + for( int i = 0; i < g_topTime; i++ ) { + SQL_FetchRow( hndl ); + TimeData data; + + data.fFinalTime = SQL_FetchFloatByName( hndl, "time" ); + data.nJumps = SQL_FetchIntByName( hndl, "jumps" ); + data.nTeleports = SQL_FetchIntByName( hndl, "teleports" ); + data.nTimestamp = SQL_FetchIntByName( hndl, "date" ); + SQL_FetchStringByName( hndl, "name", data.sName, sizeof(data.sName) ); + SQL_FetchStringByName( hndl, "steamid", data.sSteamid, sizeof(data.sSteamid) ); + SQL_FetchStringByName( hndl, "map", data.sMapName, sizeof(data.sMapName) ); + + data.nPosition = i; + + g_times[i] = data; + } +} + +public void LoadDatabase() { + char mapName[256]; + GetCurrentMap( mapName, sizeof(mapName) ); + + char query[1024]; + Format( query, sizeof(query), "SELECT * FROM times WHERE map = '%s' ORDER BY time ASC", mapName ); + SQL_TQuery( g_DB, LoadDatabaseCallback, query ); +} + +public UpdateTimeCallback( Handle owner, DBResultSet hndl, const char[] error, TimeData pack ) { + if( hndl == INVALID_HANDLE ) { + LogError( "Failed to update time: %s", error ); + return; + } + + SaveTime( pack ); +} + +public UpdateTime( TimeData data ) { + char query[1024]; + Format( query, sizeof(query), "DELETE FROM times WHERE steamid = '%s' AND map = '%s'", data.sSteamid, data.sMapName ); + DBResultSet hndl = SQL_Query( g_DB, query ); + + if( hndl == INVALID_HANDLE ) { + char err[255]; + SQL_GetError( g_DB, err, sizeof(err) ); + LogError( "Failed to update time: %s", err ); + return; + } + + SaveTime( data ); +} + +public void SaveTimeCallback( Handle owner, DBResultSet hndl, const char[] error, any pack ) { + if( hndl == INVALID_HANDLE ) { + LogError( "Failed to save time: %s", error ); + return; + } + + LoadDatabase(); +} + +public void SaveTime( TimeData data ) { + float time = data.fFinalTime; + int jumps = data.nJumps; + int teleports = data.nTeleports; + + char query[1024]; + Format( query, sizeof(query), "INSERT INTO times ( steamid, name, map, time, jumps, teleports ) VALUES ( '%s', '%s', '%s', '%f', '%d', '%d' )", data.sSteamid, data.sName, data.sMapName, time, jumps, teleports ); + SQL_TQuery( g_DB, SaveTimeCallback, query ); +} + +public void ClearRecords() { + for( int i = 0; i < MAX_TIMES; i++ ) { + g_times[i].fFinalTime = 0.0; + g_times[i].nJumps = 0; + g_times[i].nTeleports = 0; + g_times[i].nTimestamp = 0; + g_times[i].nPosition = 0; + g_times[i].sName[0] = '\0'; + g_times[i].sSteamid[0] = '\0'; + g_times[i].sMapName[0] = '\0'; + } +} + +public Action CommandTimer( Handle timer, any unused ) { + ServerCommand( "bot_kick; bot_quota 0; nh_warmup 0; sv_airaccelerate 12; mp_falldamage 0; sv_enablebunnyhopping 1; mp_ignore_round_win_conditions 1" ); + if( !g_nhWarmup ) + g_nhWarmup = FindConVar( "nh_warmup" ); + else + SetConVarInt( g_nhWarmup, 0 ); + + return Plugin_Handled; +} + +public void OnMapStart() { + ClearRecords(); + char mapName[32]; + GetCurrentMap(mapName, sizeof(mapName)); + + if( !strncmp( mapName, "kz_", 3, false ) + || !strncmp( mapName, "xc_", 3, false ) + || !strncmp( mapName, "bhop_", 5, false ) + || !strncmp( mapName, "bkz_", 4, false ) ) { + g_isKZ = true; + } else { + g_isKZ = false; + } + + ClearAllPlayers(); + + if( g_isKZ ) { + CreateTimer( 2.0, CommandTimer, 0, TIMER_FLAG_NO_MAPCHANGE ); + CreateDatabase(); + } + + PrecacheSound( "quake/standard/wickedsick.wav" ); +} + +public void OnMapEnd() { + g_isKZ = false; +} + +public void StartRun( int client ) { + MoveType mv = GetEntityMoveType( client ); + if( mv == MOVETYPE_NOCLIP ) { + EmitSoundToClient( client, "buttons/button10.wav" ); + CPrintToChat( client, "[{green}kz{default}] {red}You cannot use noclip during a run." ); + + return; + } + + EmitSoundToClient( client, "buttons/button17.wav" ); + CPrintToChat( client, "[{green}kz{default}] {white}run started." ); + g_playerData[client].bIsInRun = true; + g_playerData[client].fStartTime = GetGameTime(); + + g_playerData[client].nJumps = 0; + g_playerData[client].nTeleports = 0; + + float origin[3]; + GetClientAbsOrigin( client, origin ); + Array_Copy( origin, g_playerData[client].vStartPoint, 3 ); + + float angles[3]; + GetClientAbsAngles( client, angles ); + Array_Copy( angles, g_playerData[client].vStartAngle, 3 ); + + g_playerData[client].bSavedPoint = false; + g_playerData[client].bPausedRun = false; + + if( g_playerData[client].bShowingMenu ) { + ShowCheckpointMenu( client, true ); + } +} + +public void EndRun( int client ) { + if( !g_playerData[client].bIsInRun ) + return; + + EmitSoundToClient( client, "buttons/button9.wav" ); + g_playerData[client].bIsInRun = false; + g_playerData[client].fEndTime = GetGameTime(); + + float time = GetGameTime() - g_playerData[client].fStartTime; + int hours = RoundToFloor( time ) / 3600; + int minutes = RoundToFloor( time ) / 60; + int seconds = RoundToFloor( time ) - hours * 3600 - minutes * 60; + int milliseconds = RoundToFloor( (time - RoundToFloor( time )) * 1000 ); + + char name[64]; + GetClientName( client, name, sizeof(name) ); + + char color[16]; + strcopy( color, sizeof(color), g_playerData[client].nTeleports > 0 ? "{unique}" : "{cyan}" ); + + char chatStr[256]; + Format( chatStr, sizeof(chatStr), "[{green}kz{default}] {violet}%s {white}finished the map", name ); + + float prevRunTime = -1.0; + int runPos = -1; + char clientSteamId[32]; + GetClientAuthId( client, AuthId_Engine, clientSteamId, sizeof(clientSteamId) ); + for( int i = 0; i < g_topTime; i++ ) { + if( prevRunTime < 0 && !strcmp( clientSteamId, g_times[i].sSteamid ) ) { + prevRunTime = g_times[i].fFinalTime; + } + + if( runPos == -1 && time < g_times[i].fFinalTime ) { + runPos = i; + } + + if( runPos >= 0 && prevRunTime > 0 ) + break; + } + + if( prevRunTime < 0.0 ) + Format( chatStr, sizeof(chatStr), "%s for the first time", chatStr ); + Format( chatStr, sizeof(chatStr), "%s in %s", chatStr, color ); + + if( hours > 0 ) { + Format( chatStr, sizeof(chatStr), "%s%d:%02d:%02d.%03d", chatStr, hours, minutes, seconds, milliseconds ); + } else { + Format( chatStr, sizeof(chatStr), "%s%d:%02d.%03d", chatStr, minutes, seconds, milliseconds ); + } + + if( prevRunTime > 0.0 ) { + float diff = prevRunTime - time; + float absDiff = FloatAbs( diff ); + int diffHours = RoundToFloor( absDiff / 3600.0 ); + int diffMinutes = RoundToFloor( absDiff / 60.0 ) ; + int diffSeconds = RoundToFloor( absDiff - ( diffMinutes * 60.0 ) ) ; + int diffMilliseconds = RoundToFloor( ( absDiff - RoundToFloor( absDiff ) ) * 1000.0 ); + + if( diffHours > 0 ) { + Format( chatStr, sizeof(chatStr), "%s {white}[%s%s%d:%02d:%02d.%03d{white}]", chatStr, diff < 0 ? "{red}" : "{green}", diff < 0 ? "+" : "-", diffHours, diffMinutes, diffSeconds, diffMilliseconds ); + } else { + Format( chatStr, sizeof(chatStr), "%s {white}[%s%s%d:%02d.%03d{white}]", chatStr, diff < 0 ? "{red}" : "{green}", diff < 0 ? "+" : "-", diffMinutes, diffSeconds, diffMilliseconds ); + } + } + + if( runPos >= 0 ) { + Format( chatStr, sizeof(chatStr), "%s {white}(#%d/%d).", chatStr, runPos + 1, g_topTime ); + } else { + Format( chatStr, sizeof(chatStr), + "%s {white}(#%d/%d).", + chatStr, + g_topTime + 1, + prevRunTime > 0.0 ? g_topTime : g_topTime + 1 + ); + } + + CPrintToChatAll( chatStr ); + + if( prevRunTime < 0.0 || time < prevRunTime ) { + TimeData data; + data.fFinalTime = time; + data.nJumps = g_playerData[client].nJumps; + data.nTeleports = g_playerData[client].nTeleports; + data.nTimestamp = 0; + data.nPosition = 0; + strcopy( data.sName, sizeof(data.sName), name ); + strcopy( data.sSteamid, sizeof(data.sSteamid), clientSteamId ); + GetCurrentMap( data.sMapName, sizeof(data.sMapName) ); + + UpdateTime( data ); + + if( runPos == 0 || g_topTime == 0 ) { + CPrintToChatAll( "[{green}kz{default}] {violet}%s {white}set a new map record!", name ); + EmitSoundToAll( "quake/standard/wickedsick.wav" ); + } + } + + g_playerData[client].fStartTime = 0.0; + if( g_playerData[client].bShowingMenu ) + ShowCheckpointMenu( client, true ); +} + +public void OnButtonUsed( const char[] output, int caller, int activator, float delay ) { + if( !g_isKZ ) return; + + char entityName[64]; + GetEntPropString( caller, Prop_Data, "m_iName", entityName, sizeof(entityName) ); + + if( activator && !strcmp( entityName, "climb_startbutton" ) ) { + StartRun( activator ); + } + else if( activator && !strcmp( entityName, "climb_endbutton" ) ) { + EndRun( activator ); + } +} + +public bool AreAllPlayersOnOneTeam() { + int ctCount = 0; + int tCount = 0; + for( int i = 1; i < MaxClients; ++i ) { + if( !IsClientConnected( i ) || !IsClientInGame( i ) ) + continue; + + int team = GetClientTeam( i ); + if( team == 2 ) { + if( ctCount > 0 ) + return false; + ++tCount; + } else if( team == 3 ) { + if( tCount > 0 ) + return false; + ++ctCount; + } + } + + return true; +} + +public bool CanUseTPMenu() { + if( g_isKZ ) + return true; + + if( g_nhWarmup == null ) + g_nhWarmup = FindConVar( "nh_warmup" ); + + if( g_nhWarmup != INVALID_HANDLE && GetConVarInt( g_nhWarmup ) != 0 ) + return true; + + if( AreAllPlayersOnOneTeam() ) + return true; + + return false; +} + +public void ShowCheckpointMenu( int client, bool kz ) { + Menu menu = CreateMenu( CheckpointMenuHandler, MenuAction_Start ); + AddMenuItem( menu, "save position", "save checkpoint" ); + char buf[64]; + if( g_playerData[client].bIsInRun ) + Format( buf, sizeof(buf), "load checkpoint (%d)", g_playerData[client].nTeleports ); + else + Format( buf, sizeof(buf), "load checkpoint" ); + + AddMenuItem( menu, "load position", buf, g_playerData[client].bSavedPoint ? 0 : ITEMDRAW_DISABLED ); + + if( kz ) { + if( !g_playerData[client].bPausedRun ) + AddMenuItem( menu, "pause timer", "pause", g_playerData[client].bIsInRun ? 0 : ITEMDRAW_DISABLED ); + else + AddMenuItem( menu, "resume timer", "resume", g_playerData[client].bIsInRun ? 0 : ITEMDRAW_DISABLED ); + + AddMenuItem( menu, "respawn", "restart" ); + } + + DisplayMenu( menu, client, MENU_TIME_FOREVER ); + g_playerData[client].bShowingMenu = true; +} + +public int CheckpointMenuHandler( Menu menu, MenuAction ma, int client, int nItem ) { + if( client <= 0 || client > MAXPLAYERS ) return 0; + + if( ma == MenuAction_Select ) { + switch( nItem ) { + case 0: { Command_SavePoint( client, 0 ); } + case 1: { Command_LoadPoint( client, 0 ); } + case 2: { Command_PauseRun( client, 0 ); } + case 3: { Command_Restart( client, 0 ); } + } + + ShowCheckpointMenu( client, g_isKZ ); + return 0; + } + else if( ma == MenuAction_Cancel && nItem == -3 ) { + g_playerData[client].bShowingMenu = false; + return 0; + } + else if( ma == MenuAction_End ) { + delete menu; + } + + return 0; +} + +public Action Command_SavePoint( int client, int nargs ) { + if( !IsPlayerAlive( client ) ) { + CPrintToChat( client, "[{green}kz{default}] {red}You must be alive to use this command." ); + return Plugin_Handled; + } + + if( g_nhWarmup == null ) { + g_nhWarmup = FindConVar( "nh_warmup" ); + } + + if( !CanUseTPMenu() ) { + CPrintToChat( client, "[{green}kz{default}] {red}This command can only be used on KZ maps or during warmup." ); + return Plugin_Handled; + } + + int flags = GetEntProp( client, Prop_Send, "m_fFlags" ); + if( !( flags & FL_ONGROUND) ) { + CPrintToChat( client, "[{green}kz{default}] {red}Cannot set a checkpoint mid-air." ); + EmitSoundToClient( client, "buttons/button10.wav" ); + return Plugin_Handled; + } + + float origin[3]; + GetClientAbsOrigin( client, origin ); + Array_Copy( origin, g_playerData[client].vSavedPoint, 3 ); + Array_Copy( g_playerData[client].vLastAngle, g_playerData[client].vSavedAngles, 3 ); + + g_playerData[client].bSavedDuck = !!GetEntProp( client, Prop_Send, "m_bDucked" ); + g_playerData[client].bSavedPoint = true; + + return Plugin_Handled; +} + +public Action Command_LoadPoint( int client, int nargs ) { + if( !IsPlayerAlive( client ) ) { + CPrintToChat( client, "[{green}kz{default}] {red}You must be alive to use this command." ); + return Plugin_Handled; + } + + if( !g_playerData[client].bSavedPoint ) { + CPrintToChat( client, "[{green}kz{default}] {red}You must save your position first." ); + return Plugin_Handled; + } + + if( g_playerData[client].bPausedRun ) { + CPrintToChat( client, "[{green}kz{default}] {red}Cannot load position while the run is paused." ); + EmitSoundToClient( client, "buttons/button10.wav" ); + return Plugin_Handled; + } + + if( !CanUseTPMenu() ) { + CPrintToChat( client, "[{green}kz{default}] {red}This command can only be used on KZ maps or during warmup." ); + return Plugin_Handled; + } + + float vDiff[3], dist; + if( !g_isKZ ) { + for( int i = 1; i < MaxClients; ++i ) { + if( i == client ) + continue; + if( !IsClientConnected( i ) || !IsClientInGame( i ) || !IsPlayerAlive( i ) ) + continue; + + float origin[3]; + GetClientAbsOrigin( i, origin ); + + vDiff[0] = origin[0] - g_playerData[client].vSavedPoint[0]; + vDiff[1] = origin[1] - g_playerData[client].vSavedPoint[1]; + + dist = SquareRoot( vDiff[0] * vDiff[0] + vDiff[1] * vDiff[1] ); + if( FloatAbs( g_playerData[client].vSavedPoint[2] - origin[2] ) < 64.0 && dist < 64.0 ) { + CPrintToChat( client, "[{green}kz{default}] {red}Cannot load your position because another player is standing there." ); + return Plugin_Handled; + } + } + } + + float origin[3]; + Array_Copy( g_playerData[client].vSavedPoint, origin, 3 ); + + float angles[3]; + Array_Copy( g_playerData[client].vSavedAngles, angles, 3 ); + + float velocity[3]; + velocity[0] = 0.0; + velocity[1] = 0.0; + velocity[2] = -1.0; + + TeleportEntity( client, origin, angles, velocity ); + if( g_playerData[client].bSavedDuck ) { + SetEntProp( client, Prop_Send, "m_bDucked", 1 ); + g_playerData[client].nDuckTicks = 5; + } else { + SetEntProp( client, Prop_Send, "m_bDucked", 0 ); + } + + if( g_playerData[client].bIsInRun ) { + g_playerData[client].nTeleports++; + } + return Plugin_Handled; +} + +public Action Command_PauseRun( int client, int nargs ) { + if( !g_isKZ ) return Plugin_Handled; + + if( !g_playerData[client].bIsInRun ) { + CPrintToChat( client, "[{green}kz{default}] {red}You must be in a run to use this command." ); + return Plugin_Handled; + } + + if( !IsPlayerAlive( client ) ) { + CPrintToChat( client, "[{green}kz{default}] {red}You must be alive to use this command." ); + return Plugin_Handled; + } + + int flags = GetEntProp( client, Prop_Send, "m_fFlags" ); + if( !( flags & FL_ONGROUND) ) { + CPrintToChat( client, "[{green}kz{default}] {red}Cannot pause mid-air." ); + EmitSoundToClient( client, "buttons/button10.wav" ); + + return Plugin_Handled; + } + + if( !g_playerData[client].bPausedRun ) { + float runTime = GetGameTime() - g_playerData[client].fStartTime; + g_playerData[client].fPausedTime = runTime; + + Array_Copy( g_playerData[client].vLastAngle, g_playerData[client].vPausedAngle, 3 ); + + g_playerData[client].bPausedRun = true; + CPrintToChat( client, "[{green}kz{default}] {white}run paused." ); + } else { + g_playerData[client].fStartTime = GetGameTime() - g_playerData[client].fPausedTime; + g_playerData[client].bPausedRun = false; + + SetEntityFlags( client, GetEntityFlags( client ) & ~FL_FROZEN ); + + CPrintToChat( client, "[{green}kz{default}] {white}run resumed." ); + } + + return Plugin_Handled; +} + +public Action Command_CheckpointPanel( int client, int nargs ) { + if( !IsPlayerAlive( client ) ) { + CPrintToChat( client, "[{green}kz{default}] {red}You must be alive to use this command." ); + return Plugin_Handled; + } + + + if( !CanUseTPMenu() ) { + CPrintToChat( client, "[{green}kz{default}] {red}This command can only be used on KZ maps or during warmup." ); + return Plugin_Handled; + } + + ShowCheckpointMenu( client, g_isKZ ); + + return Plugin_Handled; +} + +public Action Command_Restart( int client, int args ) { + float origin[3]; + Array_Copy( g_playerData[client].vStartPoint, origin, 3 ); + + float angle[3]; + Array_Copy( g_playerData[client].vStartAngle, angle, 3 ); + + float velocity[3]; + velocity[0] = 0.0; + velocity[1] = 0.0; + velocity[2] = -1.0; + + TeleportEntity( client, origin, angle, velocity ); + return Plugin_Handled; +} + +public void GetTimeString( int client, char[] buffer, int size ) { + float time = 0.0; + if( g_playerData[client].bPausedRun ) { + time = g_playerData[client].fPausedTime; + } else { + time = GetGameTime() - g_playerData[client].fStartTime; + } + + int hours = RoundToFloor( time ) / 3600; + int minutes = RoundToFloor( time ) / 60; + int seconds = RoundToFloor( time ) - hours * 3600 - minutes * 60; + int milliseconds = RoundToFloor( (time - RoundToFloor( time )) * 1000 ); + + Format( buffer, size, "time: " ); + + if( hours > 0 ) { + Format( buffer, size, "%s%d:%02d:%02d.%03d", buffer, hours, minutes, seconds, milliseconds ); + } else { + Format( buffer, size, "%s%d:%02d.%03d", buffer, minutes, seconds, milliseconds ); + } + + Format( buffer, size, "%s\njumps: %d\nteleports: %d", buffer, g_playerData[client].nJumps, g_playerData[client].nTeleports ); + if( g_playerData[client].bPausedRun ) { + Format( buffer, size, "%s\n[PAUSED]", buffer ); + } +} + +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]) { + Array_Copy( angles, g_playerData[client].vLastAngle, 3 ); + if( !g_isKZ ) return Plugin_Continue; + + if( !IsPlayerAlive( client ) ) { + g_playerData[client].bIsInRun = false; + return Plugin_Continue; + } + + if( g_playerData[client].nDuckTicks > 0 ) { + buttons |= IN_DUCK; + g_playerData[client].nDuckTicks--; + } + + SetEntityRenderMode( client, RENDER_TRANSCOLOR ); + SetEntityRenderColor( client, 255, 255, 255, 100 ); + + if( GetEntityMoveType( client ) == MOVETYPE_NOCLIP && g_playerData[client].bIsInRun ) { + g_playerData[client].bIsInRun = false; + CPrintToChat( client, "[{green}kz{default}] {red}You cannot use noclip during a run." ); + EmitSoundToClient( client, "buttons/button10.wav" ); + return Plugin_Continue; + } + + if( g_playerData[client].bIsInRun ) { + char timeString[256]; + GetTimeString( client, timeString, sizeof(timeString) ); + PrintHintText( client, timeString ); + + if( g_playerData[client].bPausedRun ) { + SetEntityFlags( client, GetEntityFlags( client ) | FL_FROZEN ); + TeleportEntity( client, NULL_VECTOR, g_playerData[client].vPausedAngle, NULL_VECTOR ); + return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +public Action Event_PlayerJump( Event e, const char[] name, bool dontBroadcast ) { + int client = GetClientOfUserId( e.GetInt( "userid" ) ); + if( !g_isKZ ) return Plugin_Continue; + if( !client ) + return Plugin_Continue; + + if( !g_playerData[client].bIsInRun ) return Plugin_Continue; + + g_playerData[client].nJumps++; + return Plugin_Continue; +} + +public Action OnTakeDamage( int client, int& attacker, int& inflictor, float& damage, int& damagetype ) { + if( !g_isKZ ) return Plugin_Continue; + + if( damagetype & DMG_FALL || damagetype & DMG_BULLET ) + return Plugin_Handled; + + if( attacker > 0 && attacker < MaxClients ) { + damage = 0.0; + } + + return Plugin_Continue; +} + +public Action Event_PlayerSpawn( Event e, const char[] name, bool dontBroadcast ) { + int client = GetClientOfUserId( e.GetInt( "userid" ) ); + if( !g_isKZ ) return Plugin_Continue; + + if( !client ) + return Plugin_Continue; + + float origin[3]; + GetClientAbsOrigin( client, origin ); + + float angle[3]; + GetClientAbsAngles( client, angle ); + + Array_Copy( origin, g_playerData[client].vStartPoint, 3 ); + Array_Copy( angle, g_playerData[client].vStartAngle, 3 ); + + if( g_isKZ ) { + SetEntProp( client, Prop_Send, "m_CollisionGroup", 2 ); + ShowCheckpointMenu( client, true ); + + char cookie[32]; + g_hideWeaponCookie.Get( client, cookie, sizeof(cookie) ); + + bool draw = !StringToInt( cookie ); + g_playerData[client].bShowViewmodel = draw; + SetEntProp( client, Prop_Send, "m_bDrawViewmodel", draw ); + } + + return Plugin_Continue; +} + +public int TopTimesMenuHandler( Menu menu, MenuAction ma, int client, int nItem ) { + if( ma == MenuAction_Cancel ) { + if( g_playerData[client].bShowingMenu ) { + ShowCheckpointMenu( client, g_isKZ ); + } + } + else if( ma == MenuAction_End ) { + delete menu; + } + + return 0; +} + +public void ShowTopNubTimes( int client ) { + Menu menu = CreateMenu( TopTimesMenuHandler, MenuAction_Start ); + int max = g_topTime > 50 ? 50 : g_topTime; + for( int i = 0; i < max; i++ ) { + float time = g_times[i].fFinalTime; + int hours = RoundToFloor( time ) / 3600; + int minutes = RoundToFloor( time ) / 60; + int seconds = RoundToFloor( time ) - hours * 3600 - minutes * 60; + int milliseconds = RoundToFloor( (time - RoundToFloor( time )) * 1000 ); + + char buf[256]; + Format( buf, sizeof(buf), "%d. %s - ", i + 1, g_times[i].sName ); + if( hours > 0 ) + Format( buf, sizeof(buf), "%s%d:%02d:%02d.%03d", buf, hours, minutes, seconds, milliseconds ); + else + Format( buf, sizeof(buf), "%s%d:%02d.%03d", buf, minutes, seconds, milliseconds ); + + Format( buf, sizeof(buf), "%s (%d TP, %d jumps)", buf, g_times[i].nTeleports, g_times[i].nJumps ); + menu.AddItem( "button", buf, ITEMDRAW_DISABLED ); + } + + DisplayMenu( menu, client, MENU_TIME_FOREVER ); +} + +public void ShowTopProTimes( int client ) { + Menu menu = CreateMenu( TopTimesMenuHandler, MenuAction_Start ); + int it = 0; + int max = g_topTime > 50 ? 50 : g_topTime; + for( int i = 0; i < max; i++ ) { + if( g_times[i].nTeleports > 0 ) + continue; + + float time = g_times[i].fFinalTime; + int hours = RoundToFloor( time ) / 3600; + int minutes = RoundToFloor( time ) / 60; + int seconds = RoundToFloor( time ) - hours * 3600 - minutes * 60; + int milliseconds = RoundToFloor( (time - RoundToFloor( time )) * 1000 ); + + char buf[256]; + Format( buf, sizeof(buf), "%d. %s - ", it + 1, g_times[i].sName ); + if( hours > 0 ) + Format( buf, sizeof(buf), "%s%d:%02d:%02d.%03d", buf, hours, minutes, seconds, milliseconds ); + else + Format( buf, sizeof(buf), "%s%d:%02d.%03d", buf, minutes, seconds, milliseconds ); + Format( buf, sizeof(buf), "%s (%d TP, %d jumps)", buf, g_times[i].nTeleports, g_times[i].nJumps ); + + menu.AddItem( "button", buf, ITEMDRAW_DISABLED ); + + ++it; + } + + DisplayMenu( menu, client, MENU_TIME_FOREVER ); +} + +public int MaptopMenuHandler( Menu menu, MenuAction ma, int client, int nItem ) { + if( ma == MenuAction_Select ) { + switch( nItem ) { + case 0: { ShowTopProTimes( client ); } + case 1: { ShowTopNubTimes( client ); } + } + } else if( ma == MenuAction_Cancel ) { + if( g_playerData[client].bShowingMenu ) + ShowCheckpointMenu( client, g_isKZ ); + } + else if( ma == MenuAction_End ) { + delete menu; + } + + return 0; +} + +public Action Command_Maptop( int client, int args ) { + if( !g_isKZ ) return Plugin_Handled; + + Menu menu = CreateMenu( MaptopMenuHandler, MenuAction_Start ); + AddMenuItem( menu, "button", "top 50 PRO" ); + AddMenuItem( menu, "button", "top 50 NUB" ); + DisplayMenu( menu, client, MENU_TIME_FOREVER ); + + return Plugin_Handled; +} + +public Action Command_MyRank( int client, int args ) { + if( !g_isKZ ) return Plugin_Handled; + + char clientSteamId[32]; + GetClientAuthId( client, AuthId_Engine, clientSteamId, sizeof(clientSteamId) ); + + char name[64]; + GetClientName( client, name, sizeof(name) ); + + int rank = -1; + for( int i = 0; i < g_topTime; i++ ) { + if( !strcmp( clientSteamId, g_times[i].sSteamid ) ) { + rank = i + 1; + break; + } + } + + if( rank == -1 ) { + CPrintToChatAll( "[{green}kz{default}] {violet}%s {white}has no time on this map." ); + return Plugin_Handled; + } + + float time = g_times[rank - 1].fFinalTime; + int hours = RoundToFloor( time ) / 3600; + int minutes = RoundToFloor( time ) / 60; + int seconds = RoundToFloor( time ) - hours * 3600 - minutes * 60; + int milliseconds = RoundToFloor( (time - RoundToFloor( time )) * 1000 ); + + char buf[256]; + Format( buf, sizeof(buf), "[{green}kz{default}] {violet}%s {white}is ranked {cyan}#%d/%d{white} with a time of ", name, rank, g_topTime ); + if( hours > 0 ) + Format( buf, sizeof(buf), "%s%d:%02d:%02d.%03d", buf, hours, minutes, seconds, milliseconds ); + else + Format( buf, sizeof(buf), "%s%d:%02d.%03d", buf, minutes, seconds, milliseconds ); + + CPrintToChatAll( buf ); + return Plugin_Handled; +} + +public Action Command_HideViewmodel( int client, int args ) { + if( !g_isKZ ) return Plugin_Handled; + + bool draw = !!GetEntProp( client, Prop_Send, "m_bDrawViewmodel" ); + SetEntProp( client, Prop_Send, "m_bDrawViewmodel", !draw ); + + char cookieStr[32]; + IntToString( draw, cookieStr, sizeof(cookieStr) ); + g_hideWeaponCookie.Set( client, cookieStr ); + + g_playerData[client].bShowViewmodel = !draw; + + CPrintToChat( client, "[{green}kz{default}] viewmodel is now %s", !draw ? "shown" : "hidden" ); + return Plugin_Handled; +} + +public Action Command_Noclip( int client, int args ) { + if( !g_isKZ ) return Plugin_Handled; + + MoveType mv = GetEntityMoveType( client ); + if( mv != MOVETYPE_NOCLIP ) { + SetEntityMoveType( client, MOVETYPE_NOCLIP ); + CPrintToChat( client, "[{green}kz{default}] noclip enabled." ); + } else { + SetEntityMoveType( client, MOVETYPE_WALK ); + CPrintToChat( client, "[{green}kz{default}] noclip disabled." ); + } + + return Plugin_Handled; +} \ No newline at end of file diff --git a/source/sourcemod/scripting/distbugfix.sp b/source/sourcemod/scripting/distbugfix.sp new file mode 100644 index 0000000..d1854b5 --- /dev/null +++ b/source/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/source/sourcemod/scripting/distbugfix/clientprefs.sp b/source/sourcemod/scripting/distbugfix/clientprefs.sp new file mode 100644 index 0000000..bee4681 --- /dev/null +++ b/source/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/source/sourcemod/scripting/game_manager.sp b/source/sourcemod/scripting/game_manager.sp new file mode 100644 index 0000000..f182205 --- /dev/null +++ b/source/sourcemod/scripting/game_manager.sp @@ -0,0 +1,718 @@ +#pragma semicolon 1 +#include +#include +#include +#include +#include +#include + +#define PLUGIN_VERSION "1.0.0" +#define MAX_FILE_LEN 80 + +public Plugin:myinfo = +{ + name = "networkheaven game manager", + author = "networkheaven.net", + description = "", + version = PLUGIN_VERSION, + url = "networkheaven.net" +}; + +new g_oneChanceDone = false; +new g_maxrounds; +new g_roundCount; +new bool:halftime; +new Handle:g_h_mp_startmoney = INVALID_HANDLE; +new Handle:g_h_mp_maxrounds = INVALID_HANDLE; +new Handle:g_h_mp_buytime = INVALID_HANDLE; +new Handle:g_h_nh_warmup = INVALID_HANDLE; +new Handle:g_h_nh_buytime = INVALID_HANDLE; +new Handle:g_h_nh_teamlimit = INVALID_HANDLE; +new Handle:g_h_mp_ignoreconditions = INVALID_HANDLE; +new g_mp_startmoney; +new bool:g_doReset = false; +new g_CtScore, g_TScore; +new Handle:g_h_roundExtentTimer = INVALID_HANDLE; +new Handle:g_h_execServerCfgTimer = INVALID_HANDLE; +new g_lastDmMode = 0; +new Float:g_botVoteEnd = -1.0; +new Float:g_voteTime = 20.0; +new bool:g_hasVoted[MAXPLAYERS + 1] = {false, ...}; +new g_voteCount[9] = {0, ...}; +new bool:g_botVoteDone = false; +new bool:g_isKZ = false; + +/* forwards */ +new Handle:g_f_on_ht = INVALID_HANDLE; + +// Offsets +new g_iAccount = -1; + + +public OnMapStart(){ + halftime = false; + g_roundCount = 0; + + new String:mapName[256]; + GetCurrentMap( mapName, sizeof(mapName) ); + + if( !strncmp( mapName, "kz_", 3, false ) + || !strncmp( mapName, "xc_", 3, false ) + || !strncmp( mapName, "bhop_", 5, false ) + || !strncmp( mapName, "bkz_", 4, false ) ) { + g_isKZ = true; + } else { + g_isKZ = false; + } + + g_mp_startmoney = GetConVarInt( g_h_mp_startmoney ); + g_maxrounds = GetConVarInt( g_h_mp_maxrounds ); + + g_lastDmMode = 0; + + g_botVoteEnd = -1.0; + LogMessage( "map name: %s kz: %d", mapName, g_isKZ ); + + if( g_isKZ == false ) { + OnDmModeChanged( g_h_nh_warmup, "", "" ); + SetConVarInt( g_h_nh_warmup, 60 ); + CreateTimer( 20.0, BotVoteCreateTimer, _ ); + } +} + +public OnConfigsExecuted() { + halftime = false; +} + +public OnPluginStart() { + HookEvent("round_start", Event_RoundStart); + HookEvent("round_end", Event_RoundEnd); + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("player_team", Event_PlayerTeam); + AddCommandListener( Listener_JoinTeam, "jointeam" ); + + g_h_mp_startmoney = FindConVar( "mp_startmoney" ); + g_h_mp_maxrounds = FindConVar( "mp_maxrounds" ); + g_h_mp_buytime = FindConVar( "mp_buytime"); + g_h_mp_ignoreconditions = FindConVar( "mp_ignore_round_win_conditions" ); + g_h_nh_warmup = CreateConVar( "nh_warmup", "0", "set warmup time. -1 for infinite.", 0 ); + g_h_nh_buytime = CreateConVar( "nh_buytime", "0.5", "buytime outside of warmup.", 0 ); + g_h_nh_teamlimit = CreateConVar( "nh_teamlimit", "5", "player limit per team", 0 ); + if( g_h_nh_warmup != INVALID_HANDLE ) { + HookConVarChange( g_h_nh_warmup, OnDmModeChanged ); + } + + g_f_on_ht = CreateGlobalForward( "nthvnHalftime", ET_Ignore ); + + RegAdminCmd("sm_admbotvote", CreateBotVote, ADMFLAG_RCON); + RegConsoleCmd("sm_botvote", CreateBotVotePlayer); + + // Finding offset for CS cash + g_iAccount = FindSendPropOffs("CCSPlayer", "m_iAccount"); + if (g_iAccount == -1) + SetFailState("m_iAccount offset not found"); +} + +public OnDmModeChanged( Handle:cvar, const String:oldVal[], const String:newVal[]) { + new dm = GetConVarInt( cvar ); + + if( !!dm != !!g_lastDmMode ) { + SetConVarInt( g_h_mp_ignoreconditions, dm ); + for( new i = 1; i < MaxClients; ++i ) { + if( IsClientConnected( i ) && IsClientInGame( i ) ) + ForcePlayerSuicide( i ); + } + + ServerCommand( "mp_restartgame 1" ); + + g_CtScore = g_TScore = g_roundCount = 0; + SetTeamScore( CS_TEAM_CT, g_CtScore ); + SetTeamScore( CS_TEAM_T, g_TScore ); + } + + if( !dm ) { + SetConVarFloat( g_h_mp_buytime, GetConVarFloat( g_h_nh_buytime ) ); + if( g_h_roundExtentTimer ) { + KillTimer( g_h_roundExtentTimer ); + g_h_roundExtentTimer = INVALID_HANDLE; + } + + PrintToChatAll( "\x04===========[ game live ]===========" ); + } + else { + if( g_h_roundExtentTimer == INVALID_HANDLE ) { + g_h_roundExtentTimer = CreateTimer( 1.0, WarmupTimer, 0, TIMER_REPEAT ); + } + } + + g_lastDmMode = dm; +} + +public Action:WarmupTimer( Handle:timer, any:userid ) { + new dm = GetConVarInt( g_h_nh_warmup ); + if( dm > 0 ) { + if( dm <= 5 ) { + PrintToChatAll( "\x04========[ warmup ending in %d ]========", dm ); + } + + SetConVarInt( g_h_nh_warmup, dm - 1 ); + } + + SetConVarFloat( g_h_mp_buytime, 420.0 ); + new time = GameRules_GetProp( "m_iRoundTime" ); + if( time < 420 * 60 ) + time = 420 * 60 + 1; + GameRules_SetProp( "m_iRoundTime", time + 1, 4, 0, true ); + + new String:name[65]; + for( new i = 1; i < GetMaxEntities(); ++i ) { + if( !IsValidEdict(i) || !IsValidEntity(i) ) + continue; + + if( GetGameTime() > g_botVoteEnd ) { + if( i < MaxClients && IsClientConnected( i ) && IsClientInGame( i ) ) { + if( dm > 0 ) { + new secs = dm % 60; + new mins = (dm - secs) / 60; + PrintHintText( i, "---[ WARMUP %d:%02d ]---", mins, secs ); + } + else { + PrintHintText( i, "---[ WARMUP ]---" ); + } + + continue; + } + } + + GetEdictClassname( i, name, sizeof(name) ); + if( StrContains( name, "weapon_c4" ) != -1 ) { + RemoveEdict( i ); + } + } +} + +public OnClientPutInServer( client ) { + if( g_h_execServerCfgTimer != INVALID_HANDLE ) { + KillTimer( g_h_execServerCfgTimer ); + g_h_execServerCfgTimer = INVALID_HANDLE; + } + + SDKHook( client, SDKHook_PostThinkPost, Hook_PostThinkPost ); + + for( new i = 1; i < MaxClients; ++i ) { + if( i != client && IsClientConnected(i) && IsClientInGame(i) && !IsFakeClient(i) ) + return; + } + + if( !g_isKZ ) { + SetConVarInt( g_h_nh_warmup, 60 ); + CreateTimer( 20.0, BotVoteCreateTimer, _ ); + } +} + +public Hook_PostThinkPost( entity ) { + new dm = GetConVarInt( g_h_nh_warmup ); + + if( dm ) + SetEntProp( entity, Prop_Send, "m_bInBuyZone", 1 ); +} + +public ShowBotVoteMenu( client ) { + new Handle:hPanel = CreateMenu(BotVoteHandler); + + AddMenuItem( hPanel, "0", "vote for the amount of bots!" ); + AddMenuItem( hPanel, "1", "" ); + AddMenuItem( hPanel, "2", "" ); + AddMenuItem( hPanel, "3", "no bots" ); + AddMenuItem( hPanel, "4", "3v3 bots" ); + AddMenuItem( hPanel, "5", "5v5 bots" ); + AddMenuItem( hPanel, "6", "no choice" ); + + DisplayMenu(hPanel, client, MENU_TIME_FOREVER); +} + +public BotVoteHandler( Handle:hMenu, MenuAction:ma, client, nItem ) { + switch( ma ) { + case MenuAction_Select: + { + if( nItem == 6 ) { + g_hasVoted[client] = true; + } + else if( nItem < 3 && GetGameTime() < g_botVoteEnd ) { + ShowBotVoteMenu( client ); + } + else { + g_voteCount[nItem - 3]++; + g_hasVoted[client] = true; + } + } + + case MenuAction_End: + { + CloseHandle( hMenu ); + } + } +} + +public FinishBotVote() { + new winner = -1; + new maxvotes = 0; + for( new i = 0; i < 9; ++i ) { + if( g_voteCount[i] > maxvotes ) + winner = i; + } + + switch( winner ) { + case 0: + { + ServerCommand( "bot_kick; bot_quota 0" ); + PrintToChatAll( "Vote for no bots won, kicking all bots." ); + } + case 1: + { + ServerCommand( "bot_quota 6" ); + PrintToChatAll( "Vote for 3v3 bots won, setting quota to 6 bots." ); + } + case 2: { + ServerCommand( "bot_quota 10" ); + PrintToChatAll( "Vote for 3v3 bots won, setting quota to 10 bots." ); + } + } + + for( new i = 0; i < 9; ++i ) { + g_voteCount[i] = 0; + } + + for( new i = 0; i < MAXPLAYERS; ++i ) { + g_hasVoted[i] = false; + } +} + +public Action:BotVoteTimer( Handle:timer, any:unused ) { + new dm = GetConVarInt( g_h_nh_warmup ); + new String:hintStr[256]; + new Float:diff = g_botVoteEnd - GetGameTime(); + + if( diff <= 0 ) { + FinishBotVote(); + KillTimer( timer ); + return; + } + + for( new i = 1; i < MaxClients; ++i ) { + if( !IsClientConnected(i) || !IsClientInGame(i) || IsFakeClient(i) ) + continue; + + Format( + hintStr, + sizeof(hintStr), + "bot vote [%.0f sec left]:\nno bots: %d\n3v3 bots: %d\n5v5 bots: %d", + diff, + g_voteCount[0], + g_voteCount[1], + g_voteCount[2] + ); + + if( dm > 0 ) { + new secs = dm % 60; + new mins = (dm - secs) / 60; + Format( hintStr, sizeof(hintStr), "%s\n---[ WARMUP %d:%02d ]---", hintStr, mins, secs ); + } + else if( dm < 0 ) { + Format( hintStr, sizeof(hintStr), "%s\n---[ WARMUP ]---", hintStr ); + } + + PrintHintText( i, hintStr ); + if( !g_hasVoted[i] ) + ShowBotVoteMenu( i ); + } +} + +public Action:CreateBotVote( client, args ) { + if( GetGameTime() < g_botVoteEnd ) + return Plugin_Handled; + + if( g_isKZ ) + return Plugin_Handled; + + g_botVoteDone = true; + g_botVoteEnd = GetGameTime() + g_voteTime; + + for( new i = 0; i < 9; ++i ) { + g_voteCount[i] = 0; + } + + for( new i = 0; i < MAXPLAYERS; ++i ) { + g_hasVoted[i] = false; + } + + BotVoteTimer( INVALID_HANDLE, 0 ); + CreateTimer( 1.0, BotVoteTimer, 0, TIMER_REPEAT ); + return Plugin_Handled; +} + +public Action:CreateBotVotePlayer( client, args ) { + if( GetGameTime() < g_botVoteEnd ) { + g_hasVoted[client] = false; + ShowBotVoteMenu( client ); + } + + if( g_botVoteDone ) { + CPrintToChat( client, "{fuchsia}A bot vote has already concluded this round." ); + return Plugin_Handled; + } + + CreateBotVote( client, args ); + return Plugin_Handled; +} + +public Action:BotVoteCreateTimer( Handle: timer, any:unused ) { + CreateBotVote( 0, 0 ); +} + +public OnClientDisconnect( client ) { + if( IsFakeClient( client ) ) + return; + + for( new i = 1; i < MaxClients; ++i ) { + if( i == client ) + continue; + if( IsClientConnected( i ) && IsClientInGame( i ) && !IsFakeClient( i ) ) + return; + } + + SetConVarInt( g_h_nh_warmup, 0 ); + if( g_h_execServerCfgTimer != INVALID_HANDLE ) + KillTimer( g_h_execServerCfgTimer ); + + g_h_execServerCfgTimer = CreateTimer( 30.0, ExecServerCfg, _ ); +} + +public Action:ExecServerCfg( Handle:timer, any:unused ) { + if( g_isKZ ) + return; + + g_botVoteDone = false; + ServerCommand( "exec server.cfg" ); + g_h_execServerCfgTimer = INVALID_HANDLE; +} + +public Event_PlayerSpawn( Handle:event, const String:name[], bool dontBroadcast ) { + new dmmode = GetConVarInt( g_h_nh_warmup ); + new uid = GetEventInt( event, "userid" ); + new id = GetClientOfUserId( uid ); + + if( dmmode ) + SetEntData( id, g_iAccount, 16000, 4, true ); +} + +public Event_PlayerDeath( Handle:event, const String:name[], bool dontBroadcast ) { + CreateTimer( 0.15, OneChance, 0 ); + CreateTimer( 1.5, RespawnPlayerDeathmatch, GetEventInt( event, "userid" ) ); +} + +public Action:RespawnPlayerDeathmatch( Handle:timer, any:userid ) { + new dmmode = GetConVarInt( g_h_nh_warmup ); + if( !dmmode && !g_isKZ ) + return; + + new id = GetClientOfUserId( userid ); + if( !id ) + return; + CS_RespawnPlayer( id ); +} + +public Action:VerifyTeamCounts( Handle:timer, any:unused ) { + new entToKickT = 0; + new entToKickCT = 0; + new countCT = 0; + new countT = 0; + new maxTeam = GetConVarInt( g_h_nh_teamlimit ); + + for( new i = 1; i < MaxClients; ++i ) { + if( !IsClientConnected( i ) || !IsClientInGame( i ) ) + continue; + + + new playerTeam = GetClientTeam(i); + if( playerTeam == CS_TEAM_T ) { + ++countT; + if( IsFakeClient( i ) && (!entToKickCT || !IsPlayerAlive( i )) ) { + entToKickT = i; + } + } + if( playerTeam == CS_TEAM_CT ) { + ++countCT; + if( IsFakeClient( i ) && (!entToKickCT || !IsPlayerAlive( i )) ) { + entToKickCT = i; + } + } + } + + if( countCT > maxTeam && entToKickCT ) { + KickClient( entToKickCT, "kicked for game balance" ); + } + if( countT > maxTeam && entToKickT ) { + KickClient( entToKickT, "kicked for game balance" ); + } +} + +public bool:CanJoinTeam( id, oldTeam, newTeam ) { + new countT = 0; + new countCT = 0; + new maxTeam = GetConVarInt( g_h_nh_teamlimit ); + + for( new i = 1; i < MaxClients; ++i ) { + if( i == id ) + continue; + if( !IsClientConnected( i ) || !IsClientInGame( i ) || IsFakeClient( i ) ) + continue; + + new playerTeam = GetClientTeam(i); + if( playerTeam == CS_TEAM_T ) + ++countT; + if( playerTeam == CS_TEAM_CT ) + ++countCT; + } + + if( (newTeam == CS_TEAM_T && countT >= maxTeam) + || (newTeam == CS_TEAM_CT && countCT >= maxTeam) + ) { + return false; + } + + return true; +} + +public Action:Listener_JoinTeam( id, const String: command[], args ) { + new String:sArg1[3]; + GetCmdArg(1, sArg1, sizeof(sArg1)); + new newTeam = StringToInt(sArg1); + new oldTeam = GetClientTeam( id ); + + if( !CanJoinTeam( id, oldTeam, newTeam ) ) { + ChangeClientTeam( id, oldTeam ); + PrintHintText( id, "team is full" ); + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action:RespawnPlayerDelay( Handle: timer, any: userid ) { + new id = GetClientOfUserId( userid ); + + if( id <= 0 ) + return Plugin_Handled; + + if( !IsClientConnected( id ) || !IsClientInGame( id ) ) + return Plugin_Handled; + + if( IsPlayerAlive( id ) ) + return Plugin_Handled; + + new team = GetClientTeam( id ); + if( team < 2 ) + return Plugin_Handled; + + CS_RespawnPlayer( id ); +} + +public Action:Event_PlayerTeam( Handle:event, const String:name[], bool dontBroadcast ) { + CreateTimer( 0.2, VerifyTeamCounts, 0, 0 ); + new uid = GetEventInt( event, "userid" ); + + if( GetConVarInt( g_h_nh_warmup ) != 0 || g_isKZ ) { + CreateTimer( 0.5, RespawnPlayerDelay, uid ); + } + + return Plugin_Continue; +} + +public Action:OneChance(Handle:timer, any:unused) { + if( g_roundCount != g_maxrounds ) + return; + + if( g_oneChanceDone ) + return; + + new dm = GetConVarInt( g_h_nh_warmup ); + if( dm ) + return; + + new ctAlive = 0; + new tAlive = 0; + + new clutcherT = 0; + new clutcherCT = 0; + + for( new i = 1; i <= MaxClients; ++i ) { + if( !IsClientInGame(i) || !IsPlayerAlive(i) ) + continue; + + new playerTeam = GetClientTeam(i); + if( playerTeam == CS_TEAM_CT ) { + ++ctAlive; + if( ctAlive > 1 ) + clutcherCT = 0; + else + clutcherCT = i; + } + else if( playerTeam == CS_TEAM_T ) { + ++tAlive; + if( tAlive > 1 ) + clutcherT = 0; + else + clutcherT = i; + } + } + + if( !(tAlive == 1 && ctAlive > 2) && !(ctAlive == 1 && tAlive > 2) ) + return; + + new clutcher = clutcherT ? clutcherT : clutcherCT; + decl String:clutcherName[64]; + GetClientName( clutcher, clutcherName, sizeof(clutcherName) ); + + PrintToChatAll( "\x01%s \x04has one chance, one opportunity. will they seize it, or will they let it slip?", clutcherName ); + Client_RemoveAllWeapons( clutcher ); + Client_GiveWeapon( clutcher, "weapon_awp", false ); + Client_GiveWeapon( clutcher, "weapon_deagle", false ); + Client_GiveWeapon( clutcher, "weapon_knife", true ); + GivePlayerItem( clutcher, "weapon_flashbang" ); + GivePlayerItem( clutcher, "weapon_flashbang" ); + GivePlayerItem( clutcher, "weapon_hegrenade" ); + + SetEntityHealth( clutcher, 100 ); + Client_SetArmor( clutcher, 100 ); + SetEntProp( clutcher, Prop_Send, "m_bHasHelmet", 1, 1); + + g_oneChanceDone = true; +} + +public Event_RoundStart( Handle:event, const String:name[], bool:dontBroadcast ) +{ + new wepIdx; + new playerTeam; + + g_oneChanceDone = false; + + g_CtScore = GetTeamScore( CS_TEAM_CT ); + g_TScore = GetTeamScore( CS_TEAM_T ); + + new newRoundCount = g_CtScore + g_TScore + 1; + if( newRoundCount < g_roundCount ) { + halftime = false; + g_CtScore = 0; + g_TScore = 0; + g_roundCount = 0; + g_doReset = true; + } + else { + g_roundCount = newRoundCount; + } + + for( new client = 1; client <= MaxClients; client++ ) { + if( !IsClientConnected( client ) || !IsClientInGame( client ) ) + continue; + + else if( g_doReset ) { + for( new w = 0; w < 6; w++ ) { + if( w != 2 && w != 4 ) + while( ( wepIdx = GetPlayerWeaponSlot(client, w) ) != -1 ) + RemovePlayerItem(client, wepIdx); + } + + playerTeam = GetClientTeam( client ); + if( playerTeam == CS_TEAM_T ) { + GivePlayerItem( client, "weapon_glock" ); + } + else if( playerTeam == CS_TEAM_CT ) { + GivePlayerItem( client, "weapon_usp" ); + if ((wepIdx = GetPlayerWeaponSlot( client, 6 ) ) != -1) + RemovePlayerItem(client, wepIdx); + } + SetEntProp(client, Prop_Send, "m_ArmorValue", 0, 1); + SetEntProp(client, Prop_Send, "m_bHasHelmet", 0, 1); + + SetEntData(client, g_iAccount, g_mp_startmoney, 4, true); + } + } + + g_doReset = false; +} + +public OnGameFrame() { +} + +public Event_RoundEnd(Handle:event, const String:name[], bool:dontBroadcast) +{ + new reason = GetEventInt(event, "reason"); + new winner = GetEventInt(event, "winner"); + + g_botVoteDone = false; + g_mp_startmoney = GetConVarInt(g_h_mp_startmoney); + g_maxrounds = GetConVarInt(g_h_mp_maxrounds); + + // game commencing + if( reason == 15 ) { + g_CtScore = 0; + g_TScore = 0; + g_roundCount = 0; + return; + } + + if( winner==CS_TEAM_T ) + g_TScore++; + else if( winner==CS_TEAM_CT ) + g_CtScore++; + + if( g_TScore > g_maxrounds/2 || g_CtScore > g_maxrounds/2 ) { + SetConVarInt( g_h_mp_maxrounds, 1, true, false ); + PrintToChatAll("\x04============[ game end ]============"); + + if( g_TScore > g_CtScore ) { + PrintToChatAll("\x04Winner: T"); + } + else { + PrintToChatAll("\x04Winner: CT"); + } + } + + if( halftime || g_roundCount < g_maxrounds/2 ) { + SetTeamScore( CS_TEAM_CT, g_CtScore ); + SetTeamScore( CS_TEAM_T, g_TScore ); + return; + } + + new playerTeam; + + Call_StartForward(g_f_on_ht); + Call_Finish(); + + halftime = true; + for( new i = 1; i <= MaxClients; i++ ) { + if( IsClientInGame( i ) && IsClientConnected( i ) ) { + g_doReset = true; + + playerTeam = GetClientTeam(i); + if( playerTeam == CS_TEAM_T ) { + CS_SwitchTeam( i, CS_TEAM_CT ); + } + else if( playerTeam == CS_TEAM_CT ) { + CS_SwitchTeam( i, CS_TEAM_T ); + } + } + } + + new tmp; + tmp = g_CtScore; + g_CtScore = g_TScore; + g_TScore = tmp; + + PrintToChatAll("\x04==========[ halftime switch ]=========="); + + SetTeamScore( CS_TEAM_CT, g_CtScore ); + SetTeamScore( CS_TEAM_T, g_TScore ); +} diff --git a/source/sourcemod/scripting/gem_damage_report.sp b/source/sourcemod/scripting/gem_damage_report.sp new file mode 100644 index 0000000..97ab187 --- /dev/null +++ b/source/sourcemod/scripting/gem_damage_report.sp @@ -0,0 +1,699 @@ +/* + Description: + When you die or round ends and you are still alive + A left side menu shows who and how much you did damage to and who damaged you. + + Developed: + 2007-12-20 + + By: + [30+]Gemeni (gemeni@30plus.ownit.se) + + Version history: + 1.0.0 - First Version + 1.0.1 - Added number of hits + 1.0.2 - Added hitgroup information + 1.0.3 - Created a long and short version of the report. + If the Long version that includes hitgroups does not fit + in the panel, the short version is displayed + Added coloring in the panel + 1.1.0 - Added option so you can turn damage report on and off, and also + decide if it should be printed in menu or chat. Use /damage_report help to get info + Big thanks to death_beam team since I used their code to learn about + KeyValues! + 1.1.1 - Stripped chat output. + Resetting damage data on round start + 1.1.2 - Added total dmg taken/given and X indicator for kill/killed + 1.1.3 - Removed errors from translation stuff that was not completed + Still looking at getting translation to work + 1.1.5 - Added support to show again the last report that was shown to a player + 1.1.6 - Fixed saving of settings on map end + 1.1.7 - Problem with bogus entries in damagereportsetting file + 1.1.8 - Added most kills info + 1.1.9 - Fixed memory leak + 1.1.10 - Checking if player still ingame before trying to display. + 1.1.11 - Sometimes it seems that the settings for the users isnt written to file. Forcing this more often now. + 1.1.12 - Making sure damage data is not shown to surviving players if they have switched it off. + 1.1.13 - Added extra option, /damage_report long|short - If hitboxdata should be shown or not +*/ + +#pragma semicolon 1 +#include +#include + +// Definitions +#define MAXHITGROUPS 7 +#define NROFOPTIONS 3 +#define MAX_FILE_LEN 80 + +#define DrON 1 +#define DrOFF 0 +#define DrPop 1 +#define DrChat 0 +#define DrLong 1 +#define DrShort 0 + +#define propOnOff 0 +#define propPopChat 1 +#define propShortLong 2 + +#define PLUGIN_VERSION "1.1.13" + +public Plugin:myinfo = +{ + name = "Damage report", + author = "[30+]Gemeni, modified by networkheaven.net", + description = "Reports who damaged you and who you damaged", + version = PLUGIN_VERSION, + url = "http://30plus.ownit.se/, networkheaven.net" +}; + +// Global variables +new g_DamageDone[MAXPLAYERS+1][MAXPLAYERS+1]; +new g_HitsDone[MAXPLAYERS+1][MAXPLAYERS+1]; +new g_HitboxDone[MAXPLAYERS+1][MAXPLAYERS+1][MAXHITGROUPS+1]; + +new g_DamageTaken[MAXPLAYERS+1][MAXPLAYERS+1]; +new g_HitsTaken[MAXPLAYERS+1][MAXPLAYERS+1]; +new g_HitboxTaken[MAXPLAYERS+1][MAXPLAYERS+1][MAXHITGROUPS+1]; + +new String:g_PlayerName[MAXPLAYERS+1][32]; + +new g_KilledPlayer[MAXPLAYERS+1][MAXPLAYERS+1]; + +new g_PlayerDROption[MAXPLAYERS+1][NROFOPTIONS]; +new String:g_filenameSettings[MAX_FILE_LEN]; +new Handle:KVSettings = INVALID_HANDLE; + +new g_defaultPropOnOff = DrON; +new g_defaultPropChatPop = DrChat; +new g_defaultPropShortLong = DrShort; + +new bool:g_lateLoaded; + +// History stats +new String:g_HistDamageDone[MAXPLAYERS+1][512]; +new String:g_HistDamageDoneLong[MAXPLAYERS+1][512]; +new g_HistTotalDamageDone[MAXPLAYERS+1]; +new String:g_HistDamageTaken[MAXPLAYERS+1][512]; +new String:g_HistDamageTakenLong[MAXPLAYERS+1][512]; +new g_HistTotalDamageTaken[MAXPLAYERS+1]; + + +new Handle:g_versionConVar; + +new g_maxClients; + +new String:g_HitboxName[MAXHITGROUPS+1][20]; + +// Variable for Temp fix +new g_MenuCleared[MAXPLAYERS+1]; + + +public bool:AskPluginLoad(Handle:myself, bool:late, String:error[], err_max) +{ + g_lateLoaded = late; + return true; +} + +// Hook events on plugin start +public OnPluginStart(){ + g_versionConVar = CreateConVar("sm_damage_report_version", PLUGIN_VERSION, "Damage report version", FCVAR_PLUGIN|FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY); + SetConVarString(g_versionConVar, PLUGIN_VERSION); + HookEvent("player_death", Event_PlayerDeath); + HookEvent("player_hurt", Event_PlayerHurt); + HookEvent("player_spawn", Event_PlayerSpawn); + HookEvent("round_end", Event_RoundEnd); + HookEvent("round_start", EventRoundStart); + + HookEvent("player_disconnect", Event_PlayerDisconnect); + HookEvent("player_connect", Event_PlayerConnect); + + RegConsoleCmd("damage_report", Command_DamageReport); + RegConsoleCmd("last_damage_report", Command_LastDamageReport); + RegConsoleCmd("ldr", Command_LastDamageReport); + + KVSettings=CreateKeyValues("DamageReportSetting"); + BuildPath(Path_SM, g_filenameSettings, MAX_FILE_LEN, "data/damagereportsetting.txt"); + if(!FileToKeyValues(KVSettings, g_filenameSettings)) + { + KeyValuesToFile(KVSettings, g_filenameSettings); + } + + if(g_lateLoaded) + { + // Plugin was not loaded at beginning of round + // Find settings for all players already connected + for(new i = 1; i < g_maxClients; i++) + { + if(IsClientInGame(i) && !IsFakeClient(i)) + { + FindSettingsForClient(i); + } + } + } + g_HitboxName[0] = "Body"; + g_HitboxName[1] = "Head"; + g_HitboxName[2] = "Chest"; + g_HitboxName[3] = "Stomach"; + g_HitboxName[4] = "Left arm"; + g_HitboxName[5] = "Right arm"; + g_HitboxName[6] = "Left leg"; + g_HitboxName[7] = "Right leg"; +} + +public Action:Command_LastDamageReport(client, args) +{ + // Dont do anything if command comes from server + if (client == 0) { + return Plugin_Handled; + } + + if ((strcmp(g_HistDamageDone[client], "") != 0) || (strcmp(g_HistDamageTaken[client], "") != 0)) { + DisplayDamageReport(client, g_HistDamageDone[client] ,g_HistDamageDoneLong[client] ,g_HistDamageTaken[client], g_HistDamageTakenLong[client], g_HistTotalDamageDone[client], g_HistTotalDamageTaken[client]); + } + else { + PrintToChat(client, "\x04No history to display"); + } + + return Plugin_Handled; +} +public Action:Command_DamageReport(client, args) +{ + // Dont do anything if command comes from server + if (client == 0) { + return Plugin_Handled; + } + + // If you send in to few or to many arguments. Tell them to ask for help + if (args != 1) + { + PrintToChat(client, "\x04Usage: /damage_report help"); + return Plugin_Handled; + } + + new String:option[20]; + GetCmdArg(1, option, sizeof(option)); + + if (strcmp(option, "on", false) == 0) { + g_PlayerDROption[client][propOnOff] = DrON; + PrintToChat(client, "\x04Damage report switched on"); + } + else if (strcmp(option, "off", false) == 0) { + g_PlayerDROption[client][propOnOff] = DrOFF; + PrintToChat(client, "\x04Damage report switched off"); + } + else if (strcmp(option, "popup", false) == 0) { + g_PlayerDROption[client][propPopChat] = DrPop; + PrintToChat(client, "\x04Damage report printed in popup menu"); + } + else if (strcmp(option, "chat", false) == 0) { + g_PlayerDROption[client][propPopChat] = DrChat; + PrintToChat(client, "\x04Damage report printed in chat"); + } + else if (strcmp(option, "short", false) == 0) { + g_PlayerDROption[client][propShortLong] = DrShort; + PrintToChat(client, "\x04Hitbox data will not be shown"); + } + else if (strcmp(option, "long", false) == 0) { + g_PlayerDROption[client][propShortLong] = DrLong; + PrintToChat(client, "\x04Hitbox data will be shown"); + } + else if (strcmp(option, "status", false) == 0) { + if(g_PlayerDROption[client][propOnOff] == DrON) { + PrintToChat(client, "\x04Damage report is switched on"); + if(g_PlayerDROption[client][propPopChat] == DrPop) { + PrintToChat(client, "\x04Damage report is printed in popup menu"); + } + else { + PrintToChat(client, "\x04Damage report is printed in chat"); + } + if(g_PlayerDROption[client][propShortLong] == DrShort) { + PrintToChat(client, "\x04Hitbox data is not shown"); + } + else { + PrintToChat(client, "\x04Hitbox data is shown"); + } + } + else { + PrintToChat(client, "\x04Damage report is switched off"); + } + } + else { + new String:helpString1[200] = "/damage_report on (Turn it on) /damage_report off (Turn it off)\n"; + new String:helpString2[200] = "/damage_report popup (It will show as a left menu) /damage_report chat (It will show up in the chat)"; + new String:helpString3[200] = "/damage_report short (Will remove hitbox info)"; + new String:helpString4[200] = "/damage_report long (Will display hitbox info)"; + new String:helpString5[200] = "/damage_report status (See current damage report settings)"; + new String:helpString6[200] = "/last_damage_report or /ldr will display the last damage report that was shown to you"; + PrintToChat(client, "\x04%s", helpString1); + PrintToChat(client, "\x04%s", helpString2); + PrintToChat(client, "\x04%s", helpString3); + PrintToChat(client, "\x04%s", helpString4); + PrintToChat(client, "\x04%s", helpString5); + PrintToChat(client, "\x04%s", helpString6); + } + + StoreSettingsForClient(client); + + return Plugin_Handled; +} + + +// Save all settings on map end +public OnMapEnd() { + // Save user settings to a file + KvRewind(KVSettings); + KeyValuesToFile(KVSettings, g_filenameSettings); + + clearAllDamageData(); +} + +public OnMapStart() { + g_maxClients = GetMaxClients(); + + + // Temp fix to for clearing menues on start + for (new i=1; i<=g_maxClients; i++) + { + g_MenuCleared[i] = 0; + } +} + +// Initializations to be done at the beginning of the round +public EventRoundStart(Handle:event, const String:name[], bool:dontBroadcast) +{ + clearAllDamageData(); +} + + +// In this event we store how much damage attacker did to victim in one array +// and how much damage victim took from attacker in another array +public Event_PlayerHurt(Handle:event, const String:name[], bool:dontBroadcast) +{ + new healthDmg = GetEventInt(event,"dmg_health"); + new hitgroup = GetEventInt(event, "hitgroup"); + + new victim_id = GetEventInt(event, "userid"); + new attacker_id = GetEventInt(event, "attacker"); + + new victim = GetClientOfUserId(victim_id); + new attacker = GetClientOfUserId(attacker_id); + + // Log damage taken to the vicitm and damage done to the attacker + g_DamageDone[attacker][victim] += healthDmg; + g_HitsDone[attacker][victim]++; + g_HitboxDone[attacker][victim][hitgroup]++; + + g_DamageTaken[victim][attacker] += healthDmg; + g_HitsTaken[victim][attacker]++; + g_HitboxTaken[victim][attacker][hitgroup]++; +} + + +// Upon player dead, check who is the victim and attacker and +// Call up on the buildDamageString function +public Event_PlayerDeath(Handle:event, const String:name[], bool:dontBroadcast) +{ + new victim_id = GetEventInt(event, "userid"); + new attacker_id = GetEventInt(event, "attacker"); + + new victim = GetClientOfUserId(victim_id); + new attacker = GetClientOfUserId(attacker_id); + + g_KilledPlayer[attacker][victim]=1; + + if ((g_PlayerDROption[victim][propOnOff] == DrON) && (!IsFakeClient(victim)) && IsClientInGame(victim)) { + BuildDamageString(victim, attacker); + } +} + +// Local Function where we loop through all attackers and victims for a client. +// if any damage is taken or done a timer is called that will display a Panel with the info. +BuildDamageString (in_victim, in_attacker) { + new String:damageReport[512]; + new String:damageReportLong[600]; + new String:damageDone[512]; + new String:damageTaken[512]; + new String:damageDoneLong[512]; + new String:damageTakenLong[512]; + new String:killer[10]; + new String:xkiller[10]; + new String:killed[10]; + new String:xkilled[10]; + new totalDmgTaken, totalDmgDone; + + // Loop through all damage where you inflicted damage + for (new i=1; i<=g_maxClients; i++) + { + if(g_DamageDone[in_victim][i] >0) + { + if (g_KilledPlayer[in_victim][i] == 1) { + killed=" (Killed)"; + xkilled=" X"; + } + else { + killed=""; + xkilled=""; + } + + Format(damageDone, sizeof(damageDone), "%s%s [%d dmg, %d hits]%s\n", damageDone, g_PlayerName[i], g_DamageDone[in_victim][i], g_HitsDone[in_victim][i], xkilled); + Format(damageDoneLong, sizeof(damageDoneLong), "%s%s%s [%d dmg, %d hits]%s\n", damageDoneLong, xkilled, g_PlayerName[i], g_DamageDone[in_victim][i], g_HitsDone[in_victim][i], killed); + Format(damageDoneLong, sizeof(damageDoneLong), "%s ", damageDoneLong); + totalDmgDone += g_DamageDone[in_victim][i]; + for(new j=0; j<=MAXHITGROUPS; j++) { + if (g_HitboxDone[in_victim][i][j] > 0) { + Format(damageDoneLong, sizeof(damageDoneLong), "%s%s:%d ", damageDoneLong, g_HitboxName[j], g_HitboxDone[in_victim][i][j]); + } + } + Format(damageDoneLong, sizeof(damageDoneLong), "%s\n", damageDoneLong); + } + } + + // If you did any damage, add it to the report + if (strcmp(damageDone, "", false) != 0) { + Format(damageReport, sizeof(damageReport), "%s\n%s", "Damage done:", damageDone); + Format(damageReportLong, sizeof(damageReportLong), "%s\n%s", "Damage done:", damageDoneLong); + } + + // Loop through all damage where you took damage + for (new i=1; i<=g_maxClients; i++) + { + if(g_DamageTaken[in_victim][i] >0) + { + if (i == in_attacker) { + killer=" (Killer)"; + xkiller=" X"; + } + else { + killer=""; + xkiller=""; + } + + Format(damageTaken, sizeof(damageTaken), "%s%s [%d dmg, %d hits]%s\n", damageTaken, g_PlayerName[i], g_DamageTaken[in_victim][i], g_HitsTaken[in_victim][i], xkiller); + + Format(damageTakenLong, sizeof(damageTakenLong), "%s%s%s [%d dmg, %d hits]%s\n", damageTakenLong, xkiller, g_PlayerName[i], g_DamageTaken[in_victim][i], g_HitsTaken[in_victim][i], killer); + Format(damageTakenLong, sizeof(damageTakenLong), "%s ", damageTakenLong); + totalDmgTaken += g_DamageTaken[in_victim][i]; + for(new j=0; j<=MAXHITGROUPS; j++) { + if (g_HitboxTaken[in_victim][i][j] > 0) { + Format(damageTakenLong, sizeof(damageTakenLong), "%s%s:%d ", damageTakenLong, g_HitboxName[j], g_HitboxTaken[in_victim][i][j]); + } + } + Format(damageTakenLong, sizeof(damageTakenLong), "%s\n", damageTakenLong); + } + } + + // If you took any damage, add it to the report + if (strcmp(damageTaken, "") != 0) { + Format(damageReport, sizeof(damageReport), "%s%s\n%s", damageReport, "Damage taken:", damageTaken); + Format(damageReportLong, sizeof(damageReportLong), "%s%s\n%s", damageReportLong, "Damage taken:", damageTakenLong); + } + + // If damageReport is not empy + if (strcmp(damageReport, "") != 0) { + // store values if players what to view the last stats shown + g_HistDamageDone[in_victim] = damageDone; + g_HistDamageDoneLong[in_victim] = damageDoneLong; + g_HistTotalDamageDone[in_victim] = totalDmgDone; + + g_HistDamageTaken[in_victim] = damageTaken; + g_HistDamageTakenLong[in_victim] = damageTakenLong; + g_HistTotalDamageTaken[in_victim] = totalDmgTaken; + + DisplayDamageReport(in_victim, damageDone ,damageDoneLong ,damageTaken, damageTakenLong, totalDmgDone, totalDmgTaken); + } +} + +DisplayDamageReport(in_victim, String:damageDone[512] ,String:damageDoneLong[512] ,String:damageTaken[512], String:damageTakenLong[512], totalDmgDone, totalDmgTaken) { + if (g_PlayerDROption[in_victim][propPopChat] == DrPop) { + // Display damage report to the dead vicitm + new Handle:pack; + CreateDataTimer(1.0,DisplayDamageReportMenu, pack, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); + WritePackCell(pack, in_victim); + //LogToGame("%s", damageReport); + if (strlen(damageTakenLong)+strlen(damageDoneLong)<512-30 && (g_PlayerDROption[in_victim][propShortLong] == DrLong)) { + WritePackString(pack, damageDoneLong); + WritePackString(pack, damageTakenLong); + WritePackCell(pack, totalDmgDone); + WritePackCell(pack, totalDmgTaken); + } + else { + WritePackString(pack, damageDone); + WritePackString(pack, damageTaken); + WritePackCell(pack, totalDmgDone); + WritePackCell(pack, totalDmgTaken); + } + } + else { + if (strcmp(damageDone, "", false) != 0) { + PrintToChat(in_victim, "\n\x04==Victims (Total dmg:%d)]==\n", totalDmgDone); + PrintToChat(in_victim, "\x04%s", damageDone); + } + if (strcmp(damageTaken, "", false) != 0) { + PrintToChat(in_victim, "\x0E==[Attackers (Total dmg:%d)]==\n", totalDmgTaken); + PrintToChat(in_victim, "\x0E%s", damageTaken); + } + } +} + +// This is called by the timer. +// It checks if a menu/panel already is displayed ... if so +// let the timer try again after the delay, hoping the menu is closed +public Action:DisplayDamageReportMenu(Handle:timer, Handle:pack) { + new String:p_damageDone[512]; + new String:p_damageTaken[512]; + new String:victimItem[100]; + new String:attackerItem[100]; + new p_victim; + new p_totalDmgDone; + new p_totalDmgTaken; + + ResetPack(pack); + p_victim = ReadPackCell(pack); + ReadPackString(pack, p_damageDone, sizeof(p_damageDone)); + ReadPackString(pack, p_damageTaken, sizeof(p_damageTaken)); + p_totalDmgDone = ReadPackCell(pack); + p_totalDmgTaken = ReadPackCell(pack); + + if (!IsClientInGame(p_victim)) { + return Plugin_Stop; + } + + + if (GetClientMenu(p_victim)!=MenuSource_None) { + return Plugin_Continue; + } + + new Handle:damageReportPanel = CreatePanel(); + + if (strcmp(p_damageDone, "") != 0) { + //LogToGame("%s", p_damageReport); + + Format(victimItem, sizeof(victimItem), "Victims (Total dmg:%d)", p_totalDmgDone); + DrawPanelItem(damageReportPanel, victimItem); + DrawPanelText(damageReportPanel, p_damageDone); + } + if (strcmp(p_damageTaken, "") != 0) { + //LogToGame("%s", p_damageReport); + Format(attackerItem, sizeof(attackerItem), "Attackers (Total dmg:%d)", p_totalDmgTaken); + DrawPanelItem(damageReportPanel, attackerItem); + DrawPanelText(damageReportPanel, p_damageTaken); + } + DrawPanelItem(damageReportPanel, "Exit"); + DrawPanelText(damageReportPanel, "Type \"/damage_report help\" in chat for settings"); + + SendPanelToClient(damageReportPanel, p_victim, Handler_MyPanel, 8); + CloseHandle(damageReportPanel); + return Plugin_Stop; +} + + +// Display time timer will close my panel ... no need to handle anything +// CloseHandle seems to be called when the timer for the panel runs out +public Handler_MyPanel(Handle:menu, MenuAction:action, param1, param2) { +} + + +// Store the name of the player at time of spawn. If player disconnects before +// round end, the name can still be displayed in the damage reports. +public Event_PlayerSpawn(Handle:event, const String:name[], bool:dontBroadcast){ + new userid = GetEventInt(event,"userid"); + new client = GetClientOfUserId(userid); + // Temp fix menu + if (g_MenuCleared[client] == 0) { + new Handle:TempPanel = CreatePanel(); + DrawPanelText(TempPanel, "welcome to networkheaven"); + SendPanelToClient(TempPanel, client, Handler_MyPanel, 1); + g_MenuCleared[client] = 1; + CloseHandle(TempPanel); + } + // Store Player names if they disconnect before round has ended + new String:clientName[32]; + GetClientName(client, clientName, sizeof(clientName)); + strcopy(g_PlayerName[client], sizeof(g_PlayerName[]), clientName); + + // This shows that there is something strange when you spawn for the first time. +} + +// Temp Fix +public Event_PlayerDisconnect(Handle:event, const String:name[], bool:dontBroadcast){ + new userid = GetEventInt(event,"userid"); + new client = GetClientOfUserId(userid); + g_MenuCleared[client] = 0; + + g_HistDamageDone[client] = ""; + g_HistDamageDoneLong[client] = ""; + g_HistTotalDamageDone[client] = 0; + + g_HistDamageTaken[client] = ""; + g_HistDamageTakenLong[client] = ""; + g_HistTotalDamageTaken[client] = 0; +} + +public Event_PlayerConnect(Handle:event, const String:name[], bool:dontBroadcast){ + new userid = GetEventInt(event,"userid"); + new client = GetClientOfUserId(userid); + g_MenuCleared[client] = 0; + + g_HistDamageDone[client] = ""; + g_HistDamageDoneLong[client] = ""; + g_HistTotalDamageDone[client] = 0; + + g_HistDamageTaken[client] = ""; + g_HistDamageTakenLong[client] = ""; + g_HistTotalDamageTaken[client] = 0; +} + +public OnClientPostAdminCheck(client) { + FindSettingsForClient(client); +} + +FindSettingsForClient(client) { + new String:steamId[20]; + GetClientAuthString(client, steamId, 20); + + KvRewind(KVSettings); + if(KvJumpToKey(KVSettings, steamId)) + { + g_PlayerDROption[client][propOnOff] = KvGetNum(KVSettings, "OnOff", g_defaultPropOnOff); + g_PlayerDROption[client][propPopChat] = KvGetNum(KVSettings, "PopChat", g_defaultPropChatPop); + g_PlayerDROption[client][propShortLong] = KvGetNum(KVSettings, "ShortLong", g_defaultPropChatPop); + } else { + g_PlayerDROption[client][propOnOff] = g_defaultPropOnOff; + g_PlayerDROption[client][propPopChat] = g_defaultPropChatPop; + g_PlayerDROption[client][propShortLong] = g_defaultPropShortLong; + } + KvRewind(KVSettings); +} + +StoreSettingsForClient(client) { + new String:steamId[40]; + GetClientAuthString(client, steamId, 20); + + if(StrContains(steamId, "steam", false) != -1) { + + KvRewind(KVSettings); + if ((g_PlayerDROption[client][propOnOff] == g_defaultPropOnOff) && (g_PlayerDROption[client][propPopChat] == g_defaultPropChatPop) && (g_PlayerDROption[client][propShortLong] == g_defaultPropShortLong)) { + if(KvJumpToKey(KVSettings, steamId)) + { + KvDeleteThis(KVSettings); + } + } + else { + KvJumpToKey(KVSettings, steamId, true); + KvSetNum(KVSettings, "OnOff", g_PlayerDROption[client][propOnOff]); + KvSetNum(KVSettings, "PopChat", g_PlayerDROption[client][propPopChat]); + KvSetNum(KVSettings, "ShortLong", g_PlayerDROption[client][propShortLong]); + } + KvRewind(KVSettings); + KeyValuesToFile(KVSettings, g_filenameSettings); + } +} + +// Check if there are any living players. If so, trigger a timer for them so they will see their damage report +// Calculate who did max damage and display it as a hint. +// clear global damage done/taken arrays +public Event_RoundEnd (Handle:event, const String:name[], bool:dontBroadcast) +{ + // Make sure that we have a normal round end + // reason 16 is game commencing + // other reasons are real round ends + new reason = GetEventInt(event, "reason"); + if(reason == 16) { + return; + } + + new damage, hits, mostDamage, mostDamagePlayer, mostHits, kills, mostKills, mostKillsPlayer; + + // Display damage report to living players + // -1 in the damage string will make sure noone will be shown as the (Killer) + for (new i=1; i<=g_maxClients; i++) + { + if(IsClientInGame (i)) { + if ((g_PlayerDROption[i][propOnOff] == DrON) && IsClientConnected (i) && !IsFakeClient (i) && IsPlayerAlive (i)) { + BuildDamageString(i, -1); + } + } + } + + // Finding out who did the most damage + for (new i=1; i<=g_maxClients; i++) + { + damage = 0; + hits = 0; + for (new j=1; j<=g_maxClients; j++) + { + damage = damage + g_DamageDone[i][j]; + hits = hits + g_HitsDone[i][j]; + } + if (damage > mostDamage) { + mostDamage = damage; + mostDamagePlayer = i; + mostHits = hits; + } + } + + mostKills = 0; + for (new i=1; i<=g_maxClients; i++) + { + kills = 0; + for (new j=1; j<=g_maxClients; j++) + { + kills = kills + g_KilledPlayer[i][j]; + } + if (kills > mostKills) { + mostKills = kills; + mostKillsPlayer = i; + } + } + + // Display to all the player that did the most damage this round + if(mostDamage > 0) { + //PrintToChatAll("\x01%s\x04 inflicted most damage, \x01%d\x04 dmg in \x01%d\x04 hits!", g_PlayerName[mostDamagePlayer], mostDamage, mostHits); + } + if(mostKills > 0) { + //PrintToChatAll("\x04Most kills: \x01%s\x04 with \x01%d\x04 kills" ,g_PlayerName[mostKillsPlayer], mostKills); + } +} + +clearAllDamageData() +{ + // Clear all logged damage + for (new i=1; i<=g_maxClients; i++) + { + for (new j=1; j<=g_maxClients; j++) + { + g_DamageDone[i][j]=0; + g_DamageTaken[i][j]=0; + g_HitsDone[i][j]=0; + g_HitsTaken[i][j]=0; + g_KilledPlayer[i][j]=0; + for (new k=0; k<=MAXHITGROUPS; k++) + { + g_HitboxDone[i][j][k]=0; + g_HitboxTaken[i][j][k]=0; + } + } + } +} diff --git a/source/sourcemod/scripting/gokz-anticheat.sp b/source/sourcemod/scripting/gokz-anticheat.sp new file mode 100644 index 0000000..9925eca --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-anticheat/api.sp b/source/sourcemod/scripting/gokz-anticheat/api.sp new file mode 100644 index 0000000..7f99724 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp b/source/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp new file mode 100644 index 0000000..5607b07 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-anticheat/commands.sp b/source/sourcemod/scripting/gokz-anticheat/commands.sp new file mode 100644 index 0000000..a1fbe2e --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-chat.sp b/source/sourcemod/scripting/gokz-chat.sp new file mode 100644 index 0000000..38820f8 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core.sp b/source/sourcemod/scripting/gokz-core.sp new file mode 100644 index 0000000..897403f --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/commands.sp b/source/sourcemod/scripting/gokz-core/commands.sp new file mode 100644 index 0000000..6aba82c --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/demofix.sp b/source/sourcemod/scripting/gokz-core/demofix.sp new file mode 100644 index 0000000..84a9307 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/forwards.sp b/source/sourcemod/scripting/gokz-core/forwards.sp new file mode 100644 index 0000000..efb064f --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/buttons.sp b/source/sourcemod/scripting/gokz-core/map/buttons.sp new file mode 100644 index 0000000..8923fbd --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/end.sp b/source/sourcemod/scripting/gokz-core/map/end.sp new file mode 100644 index 0000000..d119084 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/mapfile.sp b/source/sourcemod/scripting/gokz-core/map/mapfile.sp new file mode 100644 index 0000000..db60e7e --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/prefix.sp b/source/sourcemod/scripting/gokz-core/map/prefix.sp new file mode 100644 index 0000000..3ecbf89 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/starts.sp b/source/sourcemod/scripting/gokz-core/map/starts.sp new file mode 100644 index 0000000..94d5b33 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/triggers.sp b/source/sourcemod/scripting/gokz-core/map/triggers.sp new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/map/zones.sp b/source/sourcemod/scripting/gokz-core/map/zones.sp new file mode 100644 index 0000000..684e42d --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/menus/mode_menu.sp b/source/sourcemod/scripting/gokz-core/menus/mode_menu.sp new file mode 100644 index 0000000..934d29c --- /dev/null +++ b/source/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/source/sourcemod/scripting/gokz-core/menus/options_menu.sp b/source/sourcemod/scripting/gokz-core/menus/options_menu.sp new file mode 100644 index 0000000..240ee81 --- /dev/null +++ b/source/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