summaryrefslogtreecommitdiff
path: root/sourcemod
diff options
context:
space:
mode:
authornavewindre <nw@moneybot.cc>2023-12-04 18:06:10 +0100
committernavewindre <nw@moneybot.cc>2023-12-04 18:06:10 +0100
commitaef0d1c1268ab7d4bc18996c9c6b4da16a40aadc (patch)
tree43e766b51704f4ab8b383583bdc1871eeeb9c698 /sourcemod
parent38f1140c11724da05a23a10385061200b907cf6e (diff)
bbbbbbbbwaaaaaaaaaaa
Diffstat (limited to 'sourcemod')
-rw-r--r--sourcemod/scripting/distbugfix.sp1592
-rw-r--r--sourcemod/scripting/distbugfix/clientprefs.sp51
-rw-r--r--sourcemod/scripting/gokz-anticheat.sp318
-rw-r--r--sourcemod/scripting/gokz-anticheat/api.sp174
-rw-r--r--sourcemod/scripting/gokz-anticheat/bhop_tracking.sp336
-rw-r--r--sourcemod/scripting/gokz-anticheat/commands.sp76
-rw-r--r--sourcemod/scripting/gokz-chat.sp309
-rw-r--r--sourcemod/scripting/gokz-core.sp543
-rw-r--r--sourcemod/scripting/gokz-core/commands.sp385
-rw-r--r--sourcemod/scripting/gokz-core/demofix.sp110
-rw-r--r--sourcemod/scripting/gokz-core/forwards.sp401
-rw-r--r--sourcemod/scripting/gokz-core/map/buttons.sp138
-rw-r--r--sourcemod/scripting/gokz-core/map/end.sp155
-rw-r--r--sourcemod/scripting/gokz-core/map/mapfile.sp502
-rw-r--r--sourcemod/scripting/gokz-core/map/prefix.sp48
-rw-r--r--sourcemod/scripting/gokz-core/map/starts.sp219
-rw-r--r--sourcemod/scripting/gokz-core/map/triggers.sp855
-rw-r--r--sourcemod/scripting/gokz-core/map/zones.sp183
-rw-r--r--sourcemod/scripting/gokz-core/menus/mode_menu.sp40
-rw-r--r--sourcemod/scripting/gokz-core/menus/options_menu.sp174
-rw-r--r--sourcemod/scripting/gokz-core/misc.sp803
-rw-r--r--sourcemod/scripting/gokz-core/modes.sp106
-rw-r--r--sourcemod/scripting/gokz-core/natives.sp647
-rw-r--r--sourcemod/scripting/gokz-core/options.sp438
-rw-r--r--sourcemod/scripting/gokz-core/teamnumfix.sp68
-rw-r--r--sourcemod/scripting/gokz-core/teleports.sp917
-rw-r--r--sourcemod/scripting/gokz-core/timer/pause.sp257
-rw-r--r--sourcemod/scripting/gokz-core/timer/timer.sp368
-rw-r--r--sourcemod/scripting/gokz-core/timer/virtual_buttons.sp322
-rw-r--r--sourcemod/scripting/gokz-core/triggerfix.sp622
-rw-r--r--sourcemod/scripting/gokz-errorboxfixer.sp89
-rw-r--r--sourcemod/scripting/gokz-global.sp740
-rw-r--r--sourcemod/scripting/gokz-global/api.sp142
-rw-r--r--sourcemod/scripting/gokz-global/ban_player.sp42
-rw-r--r--sourcemod/scripting/gokz-global/commands.sp169
-rw-r--r--sourcemod/scripting/gokz-global/maptop_menu.sp249
-rw-r--r--sourcemod/scripting/gokz-global/points.sp147
-rw-r--r--sourcemod/scripting/gokz-global/print_records.sp190
-rw-r--r--sourcemod/scripting/gokz-global/send_run.sp143
-rw-r--r--sourcemod/scripting/gokz-goto.sp231
-rw-r--r--sourcemod/scripting/gokz-hud.sp334
-rw-r--r--sourcemod/scripting/gokz-hud/commands.sp116
-rw-r--r--sourcemod/scripting/gokz-hud/hide_weapon.sp30
-rw-r--r--sourcemod/scripting/gokz-hud/info_panel.sp307
-rw-r--r--sourcemod/scripting/gokz-hud/menu.sp96
-rw-r--r--sourcemod/scripting/gokz-hud/natives.sp33
-rw-r--r--sourcemod/scripting/gokz-hud/options.sp190
-rw-r--r--sourcemod/scripting/gokz-hud/options_menu.sp181
-rw-r--r--sourcemod/scripting/gokz-hud/racing_text.sp167
-rw-r--r--sourcemod/scripting/gokz-hud/spectate_text.sp119
-rw-r--r--sourcemod/scripting/gokz-hud/speed_text.sp141
-rw-r--r--sourcemod/scripting/gokz-hud/timer_text.sp135
-rw-r--r--sourcemod/scripting/gokz-hud/tp_menu.sp415
-rw-r--r--sourcemod/scripting/gokz-jumpbeam.sp325
-rw-r--r--sourcemod/scripting/gokz-jumpstats.sp216
-rw-r--r--sourcemod/scripting/gokz-jumpstats/api.sp78
-rw-r--r--sourcemod/scripting/gokz-jumpstats/commands.sp28
-rw-r--r--sourcemod/scripting/gokz-jumpstats/distance_tiers.sp118
-rw-r--r--sourcemod/scripting/gokz-jumpstats/jump_reporting.sp508
-rw-r--r--sourcemod/scripting/gokz-jumpstats/jump_tracking.sp1624
-rw-r--r--sourcemod/scripting/gokz-jumpstats/jump_validating.sp82
-rw-r--r--sourcemod/scripting/gokz-jumpstats/options.sp86
-rw-r--r--sourcemod/scripting/gokz-jumpstats/options_menu.sp145
-rw-r--r--sourcemod/scripting/gokz-localdb.sp188
-rw-r--r--sourcemod/scripting/gokz-localdb/api.sp126
-rw-r--r--sourcemod/scripting/gokz-localdb/commands.sp199
-rw-r--r--sourcemod/scripting/gokz-localdb/db/cache_js.sp67
-rw-r--r--sourcemod/scripting/gokz-localdb/db/create_tables.sp36
-rw-r--r--sourcemod/scripting/gokz-localdb/db/helpers.sp18
-rw-r--r--sourcemod/scripting/gokz-localdb/db/save_js.sp291
-rw-r--r--sourcemod/scripting/gokz-localdb/db/save_time.sp83
-rw-r--r--sourcemod/scripting/gokz-localdb/db/set_cheater.sp64
-rw-r--r--sourcemod/scripting/gokz-localdb/db/setup_client.sp99
-rw-r--r--sourcemod/scripting/gokz-localdb/db/setup_database.sp34
-rw-r--r--sourcemod/scripting/gokz-localdb/db/setup_map.sp71
-rw-r--r--sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp45
-rw-r--r--sourcemod/scripting/gokz-localdb/db/sql.sp406
-rw-r--r--sourcemod/scripting/gokz-localdb/db/timer_setup.sp167
-rw-r--r--sourcemod/scripting/gokz-localdb/options.sp90
-rw-r--r--sourcemod/scripting/gokz-localranks.sp263
-rw-r--r--sourcemod/scripting/gokz-localranks/api.sp120
-rw-r--r--sourcemod/scripting/gokz-localranks/commands.sp506
-rw-r--r--sourcemod/scripting/gokz-localranks/db/cache_pbs.sp62
-rw-r--r--sourcemod/scripting/gokz-localranks/db/cache_records.sp54
-rw-r--r--sourcemod/scripting/gokz-localranks/db/create_tables.sp27
-rw-r--r--sourcemod/scripting/gokz-localranks/db/display_js.sp325
-rw-r--r--sourcemod/scripting/gokz-localranks/db/get_completion.sp155
-rw-r--r--sourcemod/scripting/gokz-localranks/db/helpers.sp91
-rw-r--r--sourcemod/scripting/gokz-localranks/db/js_top.sp286
-rw-r--r--sourcemod/scripting/gokz-localranks/db/map_top.sp388
-rw-r--r--sourcemod/scripting/gokz-localranks/db/player_top.sp165
-rw-r--r--sourcemod/scripting/gokz-localranks/db/print_average.sp152
-rw-r--r--sourcemod/scripting/gokz-localranks/db/print_js.sp108
-rw-r--r--sourcemod/scripting/gokz-localranks/db/print_pbs.sp266
-rw-r--r--sourcemod/scripting/gokz-localranks/db/print_records.sp173
-rw-r--r--sourcemod/scripting/gokz-localranks/db/process_new_time.sp157
-rw-r--r--sourcemod/scripting/gokz-localranks/db/recent_records.sp171
-rw-r--r--sourcemod/scripting/gokz-localranks/db/sql.sp411
-rw-r--r--sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp104
-rw-r--r--sourcemod/scripting/gokz-localranks/misc.sp319
-rw-r--r--sourcemod/scripting/gokz-measure.sp82
-rw-r--r--sourcemod/scripting/gokz-measure/commands.sp49
-rw-r--r--sourcemod/scripting/gokz-measure/measure_menu.sp82
-rw-r--r--sourcemod/scripting/gokz-measure/measurer.sp231
-rw-r--r--sourcemod/scripting/gokz-mode-kztimer.sp709
-rw-r--r--sourcemod/scripting/gokz-mode-simplekz.sp846
-rw-r--r--sourcemod/scripting/gokz-mode-vanilla.sp291
-rw-r--r--sourcemod/scripting/gokz-momsurffix.sp724
-rw-r--r--sourcemod/scripting/gokz-paint.sp410
-rw-r--r--sourcemod/scripting/gokz-pistol.sp303
-rw-r--r--sourcemod/scripting/gokz-playermodels.sp198
-rw-r--r--sourcemod/scripting/gokz-profile.sp396
-rw-r--r--sourcemod/scripting/gokz-profile/options.sp128
-rw-r--r--sourcemod/scripting/gokz-profile/profile.sp222
-rw-r--r--sourcemod/scripting/gokz-quiet.sp151
-rw-r--r--sourcemod/scripting/gokz-quiet/ambient.sp100
-rw-r--r--sourcemod/scripting/gokz-quiet/falldamage.sp40
-rw-r--r--sourcemod/scripting/gokz-quiet/gokz-sounds.sp71
-rw-r--r--sourcemod/scripting/gokz-quiet/hideplayers.sp309
-rw-r--r--sourcemod/scripting/gokz-quiet/options.sp206
-rw-r--r--sourcemod/scripting/gokz-quiet/soundscape.sp30
-rw-r--r--sourcemod/scripting/gokz-racing.sp174
-rw-r--r--sourcemod/scripting/gokz-racing/announce.sp229
-rw-r--r--sourcemod/scripting/gokz-racing/api.sp107
-rw-r--r--sourcemod/scripting/gokz-racing/commands.sp47
-rw-r--r--sourcemod/scripting/gokz-racing/duel_menu.sp534
-rw-r--r--sourcemod/scripting/gokz-racing/race.sp221
-rw-r--r--sourcemod/scripting/gokz-racing/race_menu.sp464
-rw-r--r--sourcemod/scripting/gokz-racing/racer.sp439
-rw-r--r--sourcemod/scripting/gokz-replays.sp397
-rw-r--r--sourcemod/scripting/gokz-replays/api.sp78
-rw-r--r--sourcemod/scripting/gokz-replays/commands.sp55
-rw-r--r--sourcemod/scripting/gokz-replays/controls.sp224
-rw-r--r--sourcemod/scripting/gokz-replays/nav.sp97
-rw-r--r--sourcemod/scripting/gokz-replays/playback.sp1501
-rw-r--r--sourcemod/scripting/gokz-replays/recording.sp990
-rw-r--r--sourcemod/scripting/gokz-replays/replay_cache.sp176
-rw-r--r--sourcemod/scripting/gokz-replays/replay_menu.sp139
-rw-r--r--sourcemod/scripting/gokz-saveloc.sp822
-rw-r--r--sourcemod/scripting/gokz-slayonend.sp190
-rw-r--r--sourcemod/scripting/gokz-spec.sp323
-rw-r--r--sourcemod/scripting/gokz-tips.sp357
-rw-r--r--sourcemod/scripting/gokz-tpanglefix.sp277
-rw-r--r--sourcemod/scripting/include/GlobalAPI.inc822
-rw-r--r--sourcemod/scripting/include/GlobalAPI/iterable.inc55
-rw-r--r--sourcemod/scripting/include/GlobalAPI/request.inc185
-rw-r--r--sourcemod/scripting/include/GlobalAPI/requestdata.inc534
-rw-r--r--sourcemod/scripting/include/GlobalAPI/responses.inc575
-rw-r--r--sourcemod/scripting/include/GlobalAPI/stocks.inc67
-rw-r--r--sourcemod/scripting/include/SteamWorks.inc413
-rw-r--r--sourcemod/scripting/include/autoexecconfig.inc765
-rw-r--r--sourcemod/scripting/include/colors.inc945
-rw-r--r--sourcemod/scripting/include/distbugfix.inc261
-rw-r--r--sourcemod/scripting/include/gamechaos.inc20
-rw-r--r--sourcemod/scripting/include/gamechaos/arrays.inc52
-rw-r--r--sourcemod/scripting/include/gamechaos/client.inc300
-rw-r--r--sourcemod/scripting/include/gamechaos/debug.inc19
-rw-r--r--sourcemod/scripting/include/gamechaos/isvalidclient.inc16
-rw-r--r--sourcemod/scripting/include/gamechaos/kreedzclimbing.inc226
-rw-r--r--sourcemod/scripting/include/gamechaos/maths.inc362
-rw-r--r--sourcemod/scripting/include/gamechaos/misc.inc245
-rw-r--r--sourcemod/scripting/include/gamechaos/strings.inc367
-rw-r--r--sourcemod/scripting/include/gamechaos/tempents.inc62
-rw-r--r--sourcemod/scripting/include/gamechaos/tracing.inc242
-rw-r--r--sourcemod/scripting/include/gamechaos/vectors.inc66
-rw-r--r--sourcemod/scripting/include/glib/addressutils.inc54
-rw-r--r--sourcemod/scripting/include/glib/assertutils.inc61
-rw-r--r--sourcemod/scripting/include/glib/memutils.inc232
-rw-r--r--sourcemod/scripting/include/gokz.inc1097
-rw-r--r--sourcemod/scripting/include/gokz/anticheat.inc168
-rw-r--r--sourcemod/scripting/include/gokz/chat.inc45
-rw-r--r--sourcemod/scripting/include/gokz/core.inc1920
-rw-r--r--sourcemod/scripting/include/gokz/global.inc317
-rw-r--r--sourcemod/scripting/include/gokz/hud.inc468
-rw-r--r--sourcemod/scripting/include/gokz/jumpbeam.inc148
-rw-r--r--sourcemod/scripting/include/gokz/jumpstats.inc442
-rw-r--r--sourcemod/scripting/include/gokz/kzplayer.inc584
-rw-r--r--sourcemod/scripting/include/gokz/localdb.inc353
-rw-r--r--sourcemod/scripting/include/gokz/localranks.inc176
-rw-r--r--sourcemod/scripting/include/gokz/momsurffix.inc23
-rw-r--r--sourcemod/scripting/include/gokz/paint.inc114
-rw-r--r--sourcemod/scripting/include/gokz/pistol.inc93
-rw-r--r--sourcemod/scripting/include/gokz/profile.inc291
-rw-r--r--sourcemod/scripting/include/gokz/quiet.inc205
-rw-r--r--sourcemod/scripting/include/gokz/racing.inc189
-rw-r--r--sourcemod/scripting/include/gokz/replays.inc275
-rw-r--r--sourcemod/scripting/include/gokz/slayonend.inc43
-rw-r--r--sourcemod/scripting/include/gokz/tips.inc59
-rw-r--r--sourcemod/scripting/include/gokz/tpanglefix.inc40
-rw-r--r--sourcemod/scripting/include/gokz/version.inc12
-rw-r--r--sourcemod/scripting/include/json.inc473
-rw-r--r--sourcemod/scripting/include/json/decode_helpers.inc312
-rw-r--r--sourcemod/scripting/include/json/definitions.inc103
-rw-r--r--sourcemod/scripting/include/json/encode_helpers.inc164
-rw-r--r--sourcemod/scripting/include/json/helpers/decode.inc502
-rw-r--r--sourcemod/scripting/include/json/helpers/encode.inc200
-rw-r--r--sourcemod/scripting/include/json/helpers/string.inc133
-rw-r--r--sourcemod/scripting/include/json/object.inc1014
-rw-r--r--sourcemod/scripting/include/json/string_helpers.inc77
-rw-r--r--sourcemod/scripting/include/movement.inc530
-rw-r--r--sourcemod/scripting/include/movementapi.inc663
-rw-r--r--sourcemod/scripting/include/smjansson.inc1328
-rw-r--r--sourcemod/scripting/include/sourcebanspp.inc106
-rw-r--r--sourcemod/scripting/include/sourcemod-colors.inc921
-rw-r--r--sourcemod/scripting/include/updater.inc97
-rw-r--r--sourcemod/scripting/momsurffix/baseplayer.sp189
-rw-r--r--sourcemod/scripting/momsurffix/gamemovement.sp411
-rw-r--r--sourcemod/scripting/momsurffix/gametrace.sp480
-rw-r--r--sourcemod/scripting/momsurffix/utils.sp279
209 files changed, 60469 insertions, 0 deletions
diff --git a/sourcemod/scripting/distbugfix.sp b/sourcemod/scripting/distbugfix.sp
new file mode 100644
index 0000000..d1854b5
--- /dev/null
+++ b/sourcemod/scripting/distbugfix.sp
@@ -0,0 +1,1592 @@
+
+#include <sourcemod>
+#include <sdktools>
+#include <sdkhooks>
+#include <clientprefs>
+#include <colors>
+
+#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 <gamechaos>
+#include <distbugfix>
+
+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[][] = {
+ "<font color='#FF00FF'>|", // overlap
+ "<font color='#000000'>|", // none
+ "<font color='#FFFFFF'>|", // left
+ "<font color='#00BFBF'>|", // overlap_left
+ "<font color='#408040'>|", // none_left
+ "<font color='#FFFFFF'>|", // right
+ "<font color='#00BFBF'>|", // overlap_right
+ "<font color='#408040'>|", // 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[][] = {
+ "<font color='#FFBF00'>|",
+ "<font color='#000000'>|",
+ "<font color='#003FFF'>|"
+ };
+ 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), "<font color='#FFFFFF'>L: ");
+ hsrIndex += strcopy(hudStrafeRight, sizeof(hudStrafeRight), "<font color='#FFFFFF'>R: ");
+ hmIndex += strcopy(hudMouse, sizeof(hudMouse), "<font color='#FFFFFF'>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), "<u><span class='fontSize-s'>%s<br>%s<br>%s", hudStrafeLeft, hudStrafeRight, hudMouse);
+
+ // TODO: sometimes just after a previous panel has faded out a new panel can't be shown, fix!
+ ShowPanel(client, 3, strafeGraph);
+ }
+ if (!IsSettingEnabled(client, SETTINGS_DISABLE_STRAFE_GRAPH))
+ {
+ ClientAndSpecsPrintConsole(client, "\nStrafe keys:\nL: %s\nR: %s", strafeLeft, strafeRight);
+ ClientAndSpecsPrintConsole(client, "Mouse movement:\nL: %s\nR: %s\n\n", mouseLeft, mouseRight);
+ }
+}
diff --git a/sourcemod/scripting/distbugfix/clientprefs.sp b/sourcemod/scripting/distbugfix/clientprefs.sp
new file mode 100644
index 0000000..bee4681
--- /dev/null
+++ b/sourcemod/scripting/distbugfix/clientprefs.sp
@@ -0,0 +1,51 @@
+
+
+static Handle distbugCookie;
+static int settings[MAXPLAYERS + 1];
+
+void OnPluginStart_Clientprefs()
+{
+ distbugCookie = RegClientCookie("distbugfix_cookie_v2", "cookie for distbugfix", CookieAccess_Private);
+ if (distbugCookie == INVALID_HANDLE)
+ {
+ SetFailState("Couldn't create distbug cookie.");
+ }
+}
+
+void OnClientCookiesCached_Clientprefs(int client)
+{
+ char buffer[MAX_COOKIE_SIZE];
+ GetClientCookie(client, distbugCookie, buffer, sizeof(buffer));
+
+ settings[client] = StringToInt(buffer);
+}
+
+void SaveClientCookies(int client)
+{
+ if (!GCIsValidClient(client) || !AreClientCookiesCached(client))
+ {
+ return;
+ }
+
+ char buffer[MAX_COOKIE_SIZE];
+ IntToString(settings[client], buffer, sizeof(buffer));
+ SetClientCookie(client, distbugCookie, buffer);
+}
+
+bool IsSettingEnabled(int client, int setting)
+{
+ if (GCIsValidClient(client))
+ {
+ return !!(settings[client] & setting);
+ }
+ return false;
+}
+
+void ToggleSetting(int client, int setting)
+{
+ if (GCIsValidClient(client))
+ {
+ settings[client] ^= setting;
+ SaveClientCookies(client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-anticheat.sp b/sourcemod/scripting/gokz-anticheat.sp
new file mode 100644
index 0000000..9925eca
--- /dev/null
+++ b/sourcemod/scripting/gokz-anticheat.sp
@@ -0,0 +1,318 @@
+#include <sourcemod>
+
+#include <dhooks>
+
+#include <movementapi>
+#include <gokz/anticheat>
+#include <gokz/core>
+
+#include <autoexecconfig>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/localdb>
+#include <sourcebanspp>
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Anti-Cheat",
+ author = "DanZay",
+ description = "Detects basic player movement cheats",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-anticheat.txt"
+
+bool gB_GOKZLocalDB;
+bool gB_SourceBansPP;
+bool gB_SourceBans;
+bool gB_GOKZGlobal;
+
+Handle gH_DHooks_OnTeleport;
+
+int gI_CmdNum[MAXPLAYERS + 1];
+int gI_LastOriginTeleportCmdNum[MAXPLAYERS + 1];
+
+int gI_ButtonCount[MAXPLAYERS + 1];
+int gI_ButtonsIndex[MAXPLAYERS + 1];
+int gI_Buttons[MAXPLAYERS + 1][AC_MAX_BUTTON_SAMPLES];
+
+int gI_BhopCount[MAXPLAYERS + 1];
+int gI_BhopIndex[MAXPLAYERS + 1];
+int gI_BhopLastTakeoffCmdnum[MAXPLAYERS + 1];
+int gI_BhopLastRecordedBhopCmdnum[MAXPLAYERS + 1];
+bool gB_BhopHitPerf[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES];
+int gI_BhopPreJumpInputs[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES];
+int gI_BhopPostJumpInputs[MAXPLAYERS + 1][AC_MAX_BHOP_SAMPLES];
+bool gB_BhopPostJumpInputsPending[MAXPLAYERS + 1];
+bool gB_LastLandingWasValid[MAXPLAYERS + 1];
+bool gB_BindExceptionPending[MAXPLAYERS + 1];
+bool gB_BindExceptionPostPending[MAXPLAYERS + 1];
+
+ConVar gCV_gokz_autoban;
+ConVar gCV_gokz_autoban_duration_bhop_hack;
+ConVar gCV_gokz_autoban_duration_bhop_macro;
+ConVar gCV_sv_autobunnyhopping;
+
+#include "gokz-anticheat/api.sp"
+#include "gokz-anticheat/bhop_tracking.sp"
+#include "gokz-anticheat/commands.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-anticheat");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("common.phrases");
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-anticheat.phrases");
+
+ CreateConVars();
+ CreateGlobalForwards();
+ HookEvents();
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZLocalDB = LibraryExists("gokz-localdb");
+ gB_SourceBansPP = LibraryExists("sourcebans++");
+ gB_SourceBans = LibraryExists("sourcebans");
+ gB_GOKZGlobal = LibraryExists("gokz-global");
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ OnClientPutInServer(client);
+ }
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZLocalDB = gB_GOKZLocalDB || StrEqual(name, "gokz-localdb");
+ gB_SourceBansPP = gB_SourceBansPP || StrEqual(name, "sourcebans++");
+ gB_SourceBans = gB_SourceBans || StrEqual(name, "sourcebans");
+ gB_GOKZGlobal = gB_GOKZGlobal || StrEqual(name, "gokz-global");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb");
+ gB_SourceBansPP = gB_SourceBansPP && !StrEqual(name, "sourcebans++");
+ gB_SourceBans = gB_SourceBans && !StrEqual(name, "sourcebans");
+ gB_GOKZGlobal = gB_GOKZGlobal && !StrEqual(name, "gokz-global");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ OnClientPutInServer_BhopTracking(client);
+ HookClientEvents(client);
+}
+
+public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2])
+{
+ gI_CmdNum[client] = cmdnum;
+ return Plugin_Continue;
+}
+
+public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2])
+{
+ if (!IsPlayerAlive(client) || IsFakeClient(client))
+ {
+ return;
+ }
+
+ OnPlayerRunCmdPost_BhopTracking(client, buttons, cmdnum);
+}
+
+public MRESReturn DHooks_OnTeleport(int client, Handle params)
+{
+ // Parameter 1 not null means origin affected
+ gI_LastOriginTeleportCmdNum[client] = !DHookIsNullParam(params, 1) ? gI_CmdNum[client] : gI_LastOriginTeleportCmdNum[client];
+
+ // Parameter 3 not null means velocity affected
+ //gI_LastVelocityTeleportCmdNum[client] = !DHookIsNullParam(params, 3) ? gI_CmdNum[client] : gI_LastVelocityTeleportCmdNum[client];
+
+ return MRES_Ignored;
+}
+
+public void GOKZ_OnFirstSpawn(int client)
+{
+ GOKZ_PrintToChat(client, false, "%t", "Anti-Cheat Warning");
+}
+
+public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats)
+{
+ LogSuspicion(client, reason, notes, stats);
+}
+
+
+
+// =====[ PUBLIC ]=====
+
+void SuspectPlayer(int client, ACReason reason, const char[] notes, const char[] stats)
+{
+ Call_OnPlayerSuspected(client, reason, notes, stats);
+
+ if (gB_GOKZLocalDB)
+ {
+ GOKZ_DB_SetCheater(client, true);
+ }
+
+ if (gCV_gokz_autoban.BoolValue)
+ {
+ BanSuspect(client, reason);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CreateConVars()
+{
+ AutoExecConfig_SetFile("gokz-anticheat", "sourcemod/gokz");
+ AutoExecConfig_SetCreateFile(true);
+
+ gCV_gokz_autoban = AutoExecConfig_CreateConVar(
+ "gokz_autoban",
+ "1",
+ "Whether to autoban players when they are suspected of cheating.",
+ _,
+ true,
+ 0.0,
+ true,
+ 1.0);
+
+ gCV_gokz_autoban_duration_bhop_hack = AutoExecConfig_CreateConVar(
+ "gokz_autoban_duration_bhop_hack",
+ "0",
+ "Duration of anticheat autobans for bunnyhop hacking in minutes (0 for permanent).",
+ _,
+ true,
+ 0.0);
+
+ gCV_gokz_autoban_duration_bhop_macro = AutoExecConfig_CreateConVar(
+ "gokz_autoban_duration_bhop_macro",
+ "43200", // 30 days
+ "Duration of anticheat autobans for bunnyhop macroing in minutes (0 for permanent).",
+ _,
+ true,
+ 0.0);
+
+ AutoExecConfig_ExecuteFile();
+ AutoExecConfig_CleanFile();
+
+ gCV_sv_autobunnyhopping = FindConVar("sv_autobunnyhopping");
+}
+
+static void HookEvents()
+{
+ GameData gameData = new GameData("sdktools.games");
+ int offset;
+
+ // Setup DHooks OnTeleport for players
+ offset = gameData.GetOffset("Teleport");
+ gH_DHooks_OnTeleport = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnTeleport);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_ObjectPtr);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_Bool);
+
+ delete gameData;
+}
+
+static void HookClientEvents(int client)
+{
+ DHookEntity(gH_DHooks_OnTeleport, true, client);
+}
+
+static void LogSuspicion(int client, ACReason reason, const char[] notes, const char[] stats)
+{
+ char logPath[PLATFORM_MAX_PATH];
+ BuildPath(Path_SM, logPath, sizeof(logPath), AC_LOG_PATH);
+
+ switch (reason)
+ {
+ case ACReason_BhopHack:LogToFileEx(logPath, "%L was suspected of bhop hacking. Notes - %s, Stats - %s", client, notes, stats);
+ case ACReason_BhopMacro:LogToFileEx(logPath, "%L was suspected of bhop macroing. Notes - %s, Stats - %s", client, notes, stats);
+ }
+}
+
+static void BanSuspect(int client, ACReason reason)
+{
+ char banMessage[128];
+ char redirectString[64] = "Contact the server administrator for more info";
+
+ if (gB_GOKZGlobal)
+ {
+ redirectString = "Visit http://rules.global-api.com/ for more info";
+ }
+
+ switch (reason)
+ {
+ case ACReason_BhopHack:
+ {
+ FormatEx(banMessage, sizeof(banMessage), "You have been banned for using a %s.\n%s", "bhop hack", redirectString);
+ AutoBanClient(
+ client,
+ gCV_gokz_autoban_duration_bhop_hack.IntValue,
+ "gokz-anticheat - Bhop hacking",
+ banMessage);
+ }
+ case ACReason_BhopMacro:
+ {
+ FormatEx(banMessage, sizeof(banMessage), "You have been banned for using a %s.\n%s", "bhop macro", redirectString);
+ AutoBanClient(
+ client,
+ gCV_gokz_autoban_duration_bhop_macro.IntValue,
+ "gokz-anticheat - Bhop macroing",
+ banMessage);
+ }
+ }
+}
+
+static void AutoBanClient(int client, int minutes, const char[] reason, const char[] kickMessage)
+{
+ if (gB_SourceBansPP)
+ {
+ SBPP_BanPlayer(0, client, minutes, reason);
+ }
+ else if (gB_SourceBans)
+ {
+ SBBanPlayer(0, client, minutes, reason);
+ }
+ else
+ {
+ BanClient(client, minutes, BANFLAG_AUTO, reason, kickMessage, "gokz-anticheat", 0);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-anticheat/api.sp b/sourcemod/scripting/gokz-anticheat/api.sp
new file mode 100644
index 0000000..7f99724
--- /dev/null
+++ b/sourcemod/scripting/gokz-anticheat/api.sp
@@ -0,0 +1,174 @@
+static GlobalForward H_OnPlayerSuspected;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnPlayerSuspected = new GlobalForward("GOKZ_AC_OnPlayerSuspected", ET_Ignore, Param_Cell, Param_Cell, Param_String, Param_String);
+}
+
+void Call_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats)
+{
+ Call_StartForward(H_OnPlayerSuspected);
+ Call_PushCell(client);
+ Call_PushCell(reason);
+ Call_PushString(notes);
+ Call_PushString(stats);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_AC_GetSampleSize", Native_GetSampleSize);
+ CreateNative("GOKZ_AC_GetHitPerf", Native_GetHitPerf);
+ CreateNative("GOKZ_AC_GetPerfCount", Native_GetPerfCount);
+ CreateNative("GOKZ_AC_GetPerfRatio", Native_GetPerfRatio);
+ CreateNative("GOKZ_AC_GetJumpInputs", Native_GetJumpInputs);
+ CreateNative("GOKZ_AC_GetAverageJumpInputs", Native_GetAverageJumpInputs);
+ CreateNative("GOKZ_AC_GetPreJumpInputs", Native_GetPreJumpInputs);
+ CreateNative("GOKZ_AC_GetPostJumpInputs", Native_GetPostJumpInputs);
+}
+
+public int Native_GetSampleSize(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ return IntMin(gI_BhopCount[client], AC_MAX_BHOP_SAMPLES);
+}
+
+public int Native_GetHitPerf(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3));
+
+ if (sampleSize == 0)
+ {
+ return 0;
+ }
+
+ bool[] perfs = new bool[sampleSize];
+ SortByRecent(gB_BhopHitPerf[client], AC_MAX_BHOP_SAMPLES, perfs, sampleSize, gI_BhopIndex[client]);
+ SetNativeArray(2, perfs, sampleSize);
+ return sampleSize;
+}
+
+public int Native_GetPerfCount(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2));
+
+ if (sampleSize == 0)
+ {
+ return 0;
+ }
+
+ bool[] perfs = new bool[sampleSize];
+ GOKZ_AC_GetHitPerf(client, perfs, sampleSize);
+
+ int perfCount = 0;
+ for (int i = 0; i < sampleSize; i++)
+ {
+ if (perfs[i])
+ {
+ perfCount++;
+ }
+ }
+ return perfCount;
+}
+
+public int Native_GetPerfRatio(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2));
+
+ if (sampleSize == 0)
+ {
+ return view_as<int>(0.0);
+ }
+
+ int perfCount = GOKZ_AC_GetPerfCount(client, sampleSize);
+ return view_as<int>(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<int>(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<int>(float(jumpInputCount) / float(sampleSize));
+}
+
+public int Native_GetPreJumpInputs(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3));
+
+ if (sampleSize == 0)
+ {
+ return 0;
+ }
+
+ int[] preJumpInputs = new int[sampleSize];
+ SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]);
+ SetNativeArray(2, preJumpInputs, sampleSize);
+ return sampleSize;
+}
+
+public int Native_GetPostJumpInputs(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3));
+
+ if (sampleSize == 0)
+ {
+ return 0;
+ }
+
+ int[] postJumpInputs = new int[sampleSize];
+ SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]);
+ SetNativeArray(2, postJumpInputs, sampleSize);
+ return sampleSize;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp
new file mode 100644
index 0000000..5607b07
--- /dev/null
+++ b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp
@@ -0,0 +1,336 @@
+/*
+ Track player's jump inputs and whether they hit perfect
+ bunnyhops for a number of their recent bunnyhops.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+void PrintBhopCheckToChat(int client, int target)
+{
+ GOKZ_PrintToChat(client, true,
+ "{lime}%N {grey}[{lime}%d%%%% {grey}%t | {lime}%.2f {grey}%t]",
+ target,
+ RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0),
+ "Perfs",
+ GOKZ_AC_GetAverageJumpInputs(target, 20),
+ "Average");
+ GOKZ_PrintToChat(client, false,
+ " {grey}%t - %s",
+ "Pattern",
+ GenerateScrollPattern(target, 20));
+}
+
+void PrintBhopCheckToConsole(int client, int target)
+{
+ PrintToConsole(client,
+ "%N [%d%% %t | %.2f %t]\n %t - %s",
+ target,
+ RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0),
+ "Perfs",
+ GOKZ_AC_GetAverageJumpInputs(target, 20),
+ "Average",
+ "Pattern",
+ GenerateScrollPattern(target, 20, false));
+}
+
+// Generate 'scroll pattern'
+char[] GenerateScrollPattern(int client, int sampleSize = AC_MAX_BHOP_SAMPLES, bool colours = true)
+{
+ char report[512];
+ int maxIndex = IntMin(gI_BhopCount[client], sampleSize);
+ bool[] perfs = new bool[maxIndex];
+ GOKZ_AC_GetHitPerf(client, perfs, maxIndex);
+ int[] jumpInputs = new int[maxIndex];
+ GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex);
+
+ for (int i = 0; i < maxIndex; i++)
+ {
+ if (colours)
+ {
+ Format(report, sizeof(report), "%s%s%d ",
+ report,
+ perfs[i] ? "{green}" : "{default}",
+ jumpInputs[i]);
+ }
+ else
+ {
+ Format(report, sizeof(report), "%s%d%s ",
+ report,
+ jumpInputs[i],
+ perfs[i] ? "*" : "");
+ }
+ }
+
+ TrimString(report);
+
+ return report;
+}
+
+// Generate 'scroll pattern' report showing pre and post inputs instead
+char[] GenerateScrollPatternEx(int client, int sampleSize = AC_MAX_BHOP_SAMPLES)
+{
+ char report[512];
+ int maxIndex = IntMin(gI_BhopCount[client], sampleSize);
+ bool[] perfs = new bool[maxIndex];
+ GOKZ_AC_GetHitPerf(client, perfs, maxIndex);
+ int[] jumpInputs = new int[maxIndex];
+ GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex);
+ int[] preJumpInputs = new int[maxIndex];
+ GOKZ_AC_GetPreJumpInputs(client, preJumpInputs, maxIndex);
+ int[] postJumpInputs = new int[maxIndex];
+ GOKZ_AC_GetPostJumpInputs(client, postJumpInputs, maxIndex);
+
+ for (int i = 0; i < maxIndex; i++)
+ {
+ Format(report, sizeof(report), "%s(%d%s%d)",
+ report,
+ preJumpInputs[i],
+ perfs[i] ? "*" : " ",
+ postJumpInputs[i]);
+ }
+
+ TrimString(report);
+
+ return report;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_BhopTracking(int client)
+{
+ ResetBhopStats(client);
+}
+
+void OnPlayerRunCmdPost_BhopTracking(int client, int buttons, int cmdnum)
+{
+ if (gCV_sv_autobunnyhopping.BoolValue)
+ {
+ return;
+ }
+
+ int nextIndex = NextIndex(gI_BhopIndex[client], AC_MAX_BHOP_SAMPLES);
+
+ // Record buttons BEFORE checking for bhop
+ RecordButtons(client, buttons);
+
+ // If bhop was last tick, then record the pre bhop inputs.
+ // Require two times the button sample size since the last
+ // takeoff to avoid pre and post bhop input overlap.
+ if (HitBhop(client, cmdnum)
+ && cmdnum >= gI_BhopLastTakeoffCmdnum[client] + AC_MAX_BUTTON_SAMPLES * 2
+ && gB_LastLandingWasValid[client])
+ {
+ gB_BhopHitPerf[client][nextIndex] = Movement_GetHitPerf(client);
+ gI_BhopPreJumpInputs[client][nextIndex] = CountJumpInputs(client);
+ gI_BhopLastRecordedBhopCmdnum[client] = cmdnum;
+ gB_BhopPostJumpInputsPending[client] = true;
+ gB_BindExceptionPending[client] = false;
+ gB_BindExceptionPostPending[client] = false;
+ }
+
+ // Bind exception
+ if (gB_BindExceptionPending[client] && cmdnum > Movement_GetLandingCmdNum(client) + AC_MAX_BHOP_GROUND_TICKS)
+ {
+ gB_BhopHitPerf[client][nextIndex] = false;
+ gI_BhopPreJumpInputs[client][nextIndex] = -1; // Special value for binded jumps
+ gI_BhopLastRecordedBhopCmdnum[client] = cmdnum;
+ gB_BhopPostJumpInputsPending[client] = true;
+ gB_BindExceptionPending[client] = false;
+ gB_BindExceptionPostPending[client] = true;
+ }
+
+ // Record post bhop inputs once enough ticks have passed
+ if (gB_BhopPostJumpInputsPending[client] && cmdnum == gI_BhopLastRecordedBhopCmdnum[client] + AC_MAX_BUTTON_SAMPLES)
+ {
+ gI_BhopPostJumpInputs[client][nextIndex] = CountJumpInputs(client);
+ gB_BhopPostJumpInputsPending[client] = false;
+ gI_BhopIndex[client] = nextIndex;
+ gI_BhopCount[client]++;
+ CheckForBhopMacro(client);
+ gB_BindExceptionPostPending[client] = false;
+ }
+
+ // Record last jump takeoff time
+ if (JustJumped(client, cmdnum))
+ {
+ gI_BhopLastTakeoffCmdnum[client] = cmdnum;
+ gB_BindExceptionPending[client] = false;
+ if (gB_BindExceptionPostPending[client])
+ {
+ gB_BhopPostJumpInputsPending[client] = false;
+ gB_BindExceptionPostPending[client] = false;
+ }
+ }
+
+ if (JustLanded(client, cmdnum))
+ {
+ // These conditions exist to reduce false positives.
+
+ // Telehopping is when the player bunnyhops out of a teleport that has a
+ // destination very close to the ground. This will, more than usual,
+ // result in a perfect bunnyhop. This is alleviated by checking if the
+ // player's origin was affected by a teleport last tick.
+
+ // When a player is pressing up against a slope but not ascending it (e.g.
+ // palm trees on kz_adv_cursedjourney), they will switch between on ground
+ // and off ground frequently, which means that if they manage to jump, the
+ // jump will be recorded as a perfect bunnyhop. To ignore this, we check
+ // the jump is more than 1 tick duration.
+
+ gB_LastLandingWasValid[client] = cmdnum - gI_LastOriginTeleportCmdNum[client] > 1
+ && cmdnum - Movement_GetTakeoffCmdNum(client) > 1;
+
+ // You can still crouch-bind VNL jumps and some people just don't know that
+ // it doesn't work with the other modes in GOKZ. This can cause false positives
+ // if the player uses the bind for bhops and mostly presses it too early or
+ // exactly on time rather than too late. This is supposed to reduce those by
+ // detecting jumps where you don't get a bhop and have exactly one jump input
+ // before landing and none after landing. We require the one input to be right
+ // before the jump to make it a lot harder to fake a binded jump when doing
+ // a regular longjump.
+ gB_BindExceptionPending[client] = (CountJumpInputs(client, AC_BINDEXCEPTION_SAMPLES) == 1 && CountJumpInputs(client, AC_MAX_BUTTON_SAMPLES) == 1);
+ gB_BindExceptionPostPending[client] = false;
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CheckForBhopMacro(int client)
+{
+ if (GOKZ_AC_GetPerfCount(client, 19) == 19)
+ {
+ SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 19));
+ }
+ else if (GOKZ_AC_GetPerfCount(client, 30) >= 28)
+ {
+ SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 30));
+ }
+ else if (GOKZ_AC_GetPerfCount(client, 20) >= 16 && GOKZ_AC_GetAverageJumpInputs(client, 20) <= 2.0 + EPSILON)
+ {
+ SuspectPlayer(client, ACReason_BhopHack, "1's or 2's scroll pattern", GenerateBhopBanStats(client, 20));
+ }
+ else if (gI_BhopCount[client] >= 20 && GOKZ_AC_GetPerfCount(client, 20) >= 8
+ && GOKZ_AC_GetAverageJumpInputs(client, 20) >= 19.0 - EPSILON)
+ {
+ SuspectPlayer(client, ACReason_BhopMacro, "High scroll pattern", GenerateBhopBanStats(client, 20));
+ }
+ else if (GOKZ_AC_GetPerfCount(client, 30) >= 10 && CheckForRepeatingJumpInputsCount(client, 25, 30) >= 14)
+ {
+ SuspectPlayer(client, ACReason_BhopMacro, "Repeating scroll pattern", GenerateBhopBanStats(client, 30));
+ }
+}
+
+static char[] GenerateBhopBanStats(int client, int sampleSize)
+{
+ char stats[512];
+ FormatEx(stats, sizeof(stats),
+ "Perfs: %d/%d, Average: %.2f, Scroll pattern: %s",
+ GOKZ_AC_GetPerfCount(client, sampleSize),
+ IntMin(gI_BhopCount[client], sampleSize),
+ GOKZ_AC_GetAverageJumpInputs(client, sampleSize),
+ GenerateScrollPatternEx(client, sampleSize));
+ return stats;
+}
+
+/**
+ * Returns -1, or the repeating input count if there if there is
+ * an input count that repeats for more than the provided ratio.
+ *
+ * @param client Client index.
+ * @param threshold Minimum frequency to be considered 'repeating'.
+ * @param sampleSize Maximum recent bhop samples to include in calculation.
+ * @return The repeating input, or else -1.
+ */
+static int CheckForRepeatingJumpInputsCount(int client, int threshold, int sampleSize = AC_MAX_BHOP_SAMPLES)
+{
+ int maxIndex = IntMin(gI_BhopCount[client], sampleSize);
+ int[] jumpInputs = new int[maxIndex];
+ GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex);
+ int maxJumpInputs = AC_MAX_BUTTON_SAMPLES + 1;
+ int[] jumpInputsFrequency = new int[maxJumpInputs];
+
+ // Count up all the in jump patterns
+ for (int i = 0; i < maxIndex; i++)
+ {
+ // -1 is a binded jump, those are excluded
+ if (jumpInputs[i] != -1)
+ {
+ jumpInputsFrequency[jumpInputs[i]]++;
+ }
+ }
+
+ // Returns i if the given number of the sample size has the same jump input count
+ for (int i = 1; i < maxJumpInputs; i++)
+ {
+ if (jumpInputsFrequency[i] >= threshold)
+ {
+ return i;
+ }
+ }
+
+ return -1; // -1 if no repeating jump input found
+}
+
+// Reset the tracked bhop stats of the client
+static void ResetBhopStats(int client)
+{
+ gI_ButtonCount[client] = 0;
+ gI_ButtonsIndex[client] = 0;
+ gI_BhopCount[client] = 0;
+ gI_BhopIndex[client] = 0;
+ gI_BhopLastTakeoffCmdnum[client] = 0;
+ gI_BhopLastRecordedBhopCmdnum[client] = 0;
+ gB_BhopPostJumpInputsPending[client] = false;
+ gB_LastLandingWasValid[client] = false;
+ gB_BindExceptionPending[client] = false;
+ gB_BindExceptionPostPending[client] = false;
+}
+
+// Returns true if ther was a jump last tick and was within a number of ticks after landing
+static bool HitBhop(int client, int cmdnum)
+{
+ return JustJumped(client, cmdnum) && Movement_GetTakeoffCmdNum(client) - Movement_GetLandingCmdNum(client) <= AC_MAX_BHOP_GROUND_TICKS;
+}
+
+static bool JustJumped(int client, int cmdnum)
+{
+ return Movement_GetJumped(client) && Movement_GetTakeoffCmdNum(client) == cmdnum;
+}
+
+static bool JustLanded(int client, int cmdnum)
+{
+ return Movement_GetLandingCmdNum(client) == cmdnum;
+}
+
+// Records current button inputs
+static void RecordButtons(int client, int buttons)
+{
+ gI_ButtonsIndex[client] = NextIndex(gI_ButtonsIndex[client], AC_MAX_BUTTON_SAMPLES);
+ gI_Buttons[client][gI_ButtonsIndex[client]] = buttons;
+ gI_ButtonCount[client]++;
+}
+
+// Counts the number of times buttons went from !IN_JUMP to IN_JUMP
+static int CountJumpInputs(int client, int sampleSize = AC_MAX_BUTTON_SAMPLES)
+{
+ int[] recentButtons = new int[sampleSize];
+ SortByRecent(gI_Buttons[client], AC_MAX_BUTTON_SAMPLES, recentButtons, sampleSize, gI_ButtonsIndex[client]);
+ int maxIndex = IntMin(gI_ButtonCount[client], sampleSize);
+ int jumps = 0;
+
+ for (int i = 0; i < maxIndex - 1; i++)
+ {
+ // If buttons went from !IN_JUMP to IN_JUMP
+ if (!(recentButtons[i + 1] & IN_JUMP) && recentButtons[i] & IN_JUMP)
+ {
+ jumps++;
+ }
+ }
+ return jumps;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-anticheat/commands.sp b/sourcemod/scripting/gokz-anticheat/commands.sp
new file mode 100644
index 0000000..a1fbe2e
--- /dev/null
+++ b/sourcemod/scripting/gokz-anticheat/commands.sp
@@ -0,0 +1,76 @@
+void RegisterCommands()
+{
+ RegAdminCmd("sm_bhopcheck", CommandBhopCheck, ADMFLAG_ROOT, "[KZ] Show bunnyhop stats report including perf ratio and scroll pattern.");
+}
+
+public Action CommandBhopCheck(int client, int args)
+{
+ if (args == 0)
+ {
+ if (GOKZ_AC_GetSampleSize(client) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)");
+ }
+ else
+ {
+ PrintBhopCheckToChat(client, client);
+ }
+ return Plugin_Handled;
+ }
+
+ char arg[65];
+ GetCmdArg(1, arg, sizeof(arg));
+ char targetName[MAX_TARGET_LENGTH];
+ int targetList[MAXPLAYERS], targetCount;
+ bool tnIsML;
+
+ if ((targetCount = ProcessTargetString(
+ arg,
+ client,
+ targetList,
+ MAXPLAYERS,
+ COMMAND_FILTER_NO_IMMUNITY | COMMAND_FILTER_NO_BOTS,
+ targetName,
+ sizeof(targetName),
+ tnIsML)) <= 0)
+ {
+ ReplyToTargetError(client, targetCount);
+ return Plugin_Handled;
+ }
+
+ if (targetCount >= 2)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "See Console");
+ for (int i = 0; i < targetCount; i++)
+ {
+ if (GOKZ_AC_GetSampleSize(targetList[i]) == 0)
+ {
+ PrintToConsole(client, "%t", "Not Enough Bhops (Console)", targetList[i]);
+ }
+ else
+ {
+ PrintBhopCheckToConsole(client, targetList[i]);
+ }
+ }
+ }
+ else
+ {
+ if (GOKZ_AC_GetSampleSize(targetList[0]) == 0)
+ {
+ if (targetList[0] == client)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops", targetList[0]);
+ }
+ }
+ else
+ {
+ PrintBhopCheckToChat(client, targetList[0]);
+ }
+ }
+
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-chat.sp b/sourcemod/scripting/gokz-chat.sp
new file mode 100644
index 0000000..38820f8
--- /dev/null
+++ b/sourcemod/scripting/gokz-chat.sp
@@ -0,0 +1,309 @@
+#include <sourcemod>
+
+#include <cstrike>
+
+#include <gokz/core>
+
+#include <autoexecconfig>
+#include <morecolors>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <basecomm>
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Chat",
+ author = "DanZay",
+ description = "Handles client-triggered chat messages",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-chat.txt"
+
+bool gB_BaseComm;
+char gC_PlayerTags[MAXPLAYERS + 1][32];
+char gC_PlayerTagColors[MAXPLAYERS + 1][16];
+
+ConVar gCV_gokz_chat_processing;
+ConVar gCV_gokz_connection_messages;
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-chat");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-chat.phrases");
+
+ CreateConVars();
+ HookEvents();
+
+ OnPluginStart_BlockRadio();
+ OnPluginStart_BlockChatWheel();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_BaseComm = LibraryExists("basecomm");
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_BaseComm = gB_BaseComm || StrEqual(name, "basecomm");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_BaseComm = gB_BaseComm && !StrEqual(name, "basecomm");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
+{
+ if (client > 0 && gCV_gokz_chat_processing.BoolValue && IsClientInGame(client))
+ {
+ OnClientSayCommand_ChatProcessing(client, command, sArgs);
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+public void OnClientConnected(int client)
+{
+ gC_PlayerTags[client][0] = '\0';
+ gC_PlayerTagColors[client][0] = '\0';
+}
+
+public void OnClientPutInServer(int client)
+{
+ PrintConnectMessage(client);
+}
+
+public Action OnPlayerDisconnect(Event event, const char[] name, bool dontBroadcast) // player_disconnect pre hook
+{
+ event.BroadcastDisabled = true; // Block disconnection messages
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ PrintDisconnectMessage(client, event);
+ }
+ return Plugin_Continue;
+}
+
+public Action OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast) // player_team pre hook
+{
+ event.SetBool("silent", true); // Block join team messages
+ return Plugin_Continue;
+}
+
+
+
+// =====[ GENERAL ]=====
+
+void CreateConVars()
+{
+ AutoExecConfig_SetFile("gokz-chat", "sourcemod/gokz");
+ AutoExecConfig_SetCreateFile(true);
+
+ gCV_gokz_chat_processing = AutoExecConfig_CreateConVar("gokz_chat_processing", "1", "Whether GOKZ processes player chat messages.", _, true, 0.0, true, 1.0);
+ gCV_gokz_connection_messages = AutoExecConfig_CreateConVar("gokz_connection_messages", "1", "Whether GOKZ handles connection and disconnection messages.", _, true, 0.0, true, 1.0);
+
+ AutoExecConfig_ExecuteFile();
+ AutoExecConfig_CleanFile();
+}
+
+void HookEvents()
+{
+ HookEvent("player_disconnect", OnPlayerDisconnect, EventHookMode_Pre);
+ HookEvent("player_team", OnPlayerJoinTeam, EventHookMode_Pre);
+}
+
+
+
+// =====[ CHAT PROCESSING ]=====
+
+void OnClientSayCommand_ChatProcessing(int client, const char[] command, const char[] message)
+{
+ if (gB_BaseComm && BaseComm_IsClientGagged(client)
+ || UsedBaseChat(client, command, message))
+ {
+ return;
+ }
+
+ // Resend messages that may have been a command with capital letters
+ if ((message[0] == '!' || message[0] == '/') && IsCharUpper(message[1]))
+ {
+ char loweredMessage[128];
+ String_ToLower(message, loweredMessage, sizeof(loweredMessage));
+ FakeClientCommand(client, "say %s", loweredMessage);
+ return;
+ }
+
+ char sanitisedMessage[128];
+ strcopy(sanitisedMessage, sizeof(sanitisedMessage), message);
+ SanitiseChatInput(sanitisedMessage, sizeof(sanitisedMessage));
+
+ char sanitisedName[MAX_NAME_LENGTH];
+ GetClientName(client, sanitisedName, sizeof(sanitisedName));
+ SanitiseChatInput(sanitisedName, sizeof(sanitisedName));
+
+ if (TrimString(sanitisedMessage) == 0)
+ {
+ return;
+ }
+
+ if (IsSpectating(client))
+ {
+ GOKZ_PrintToChatAll(false, "{default}* %s%s{lime}%s{default} : %s",
+ gC_PlayerTagColors[client], gC_PlayerTags[client], sanitisedName, sanitisedMessage);
+ PrintToConsoleAll("* %s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage);
+ PrintToServer("* %s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(false, "%s%s{lime}%s{default} : %s",
+ gC_PlayerTagColors[client], gC_PlayerTags[client], sanitisedName, sanitisedMessage);
+ PrintToConsoleAll("%s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage);
+ PrintToServer("%s%s : %s", gC_PlayerTags[client], sanitisedName, sanitisedMessage);
+ }
+}
+
+bool UsedBaseChat(int client, const char[] command, const char[] message)
+{
+ // Assuming base chat is in use, check if message will get processed by basechat
+ if (message[0] != '@')
+ {
+ return false;
+ }
+
+ if (strcmp(command, "say_team", false) == 0)
+ {
+ return true;
+ }
+ else if (strcmp(command, "say", false) == 0 && CheckCommandAccess(client, "sm_say", ADMFLAG_CHAT))
+ {
+ return true;
+ }
+
+ return false;
+}
+
+void SanitiseChatInput(char[] message, int maxlength)
+{
+ Color_StripFromChatText(message, message, maxlength);
+ //CRemoveColors(message, maxlength);
+ // Chat gets double formatted, so replace '%' with '%%%%' to end up with '%'
+ ReplaceString(message, maxlength, "%", "%%%%");
+}
+
+
+
+// =====[ CONNECTION MESSAGES ]=====
+
+void PrintConnectMessage(int client)
+{
+ if (!gCV_gokz_connection_messages.BoolValue || IsFakeClient(client))
+ {
+ return;
+ }
+
+ GOKZ_PrintToChatAll(false, "%t", "Client Connection Message", client);
+}
+
+void PrintDisconnectMessage(int client, Event event) // Hooked to player_disconnect event
+{
+ if (!gCV_gokz_connection_messages.BoolValue || IsFakeClient(client))
+ {
+ return;
+ }
+
+ char reason[128];
+ event.GetString("reason", reason, sizeof(reason));
+ GOKZ_PrintToChatAll(false, "%t", "Client Disconnection Message", client, reason);
+}
+
+
+
+// =====[ BLOCK RADIO AND CHATWHEEL]=====
+
+static char radioCommands[][] =
+{
+ "coverme", "takepoint", "holdpos", "regroup", "followme", "takingfire", "go",
+ "fallback", "sticktog", "getinpos", "stormfront", "report", "roger", "enemyspot",
+ "needbackup", "sectorclear", "inposition", "reportingin", "getout", "negative",
+ "enemydown", "compliment", "thanks", "cheer", "go_a", "go_b", "sorry", "needrop"
+};
+
+public void OnPluginStart_BlockRadio()
+{
+ for (int i = 0; i < sizeof(radioCommands); i++)
+ {
+ AddCommandListener(CommandBlock, radioCommands[i]);
+ }
+}
+
+public void OnPluginStart_BlockChatWheel()
+{
+ AddCommandListener(CommandBlock, "playerchatwheel");
+ AddCommandListener(CommandBlock, "chatwheel_ping");
+}
+
+public Action CommandBlock(int client, const char[] command, int argc)
+{
+ return Plugin_Handled;
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_CH_SetChatTag", Native_SetChatTag);
+}
+
+public int Native_SetChatTag(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+
+ char str[64];
+ GetNativeString(2, str, sizeof(str));
+ if (str[0] == '\0')
+ {
+ // To prevent the space after the mode
+ FormatEx(gC_PlayerTags[client], sizeof(gC_PlayerTags[]), "[%s] ", gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ else
+ {
+ FormatEx(gC_PlayerTags[client], sizeof(gC_PlayerTags[]), "[%s %s] ", gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)], str);
+ }
+
+ GetNativeString(3, gC_PlayerTagColors[client], sizeof(gC_PlayerTagColors[]));
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-core.sp b/sourcemod/scripting/gokz-core.sp
new file mode 100644
index 0000000..897403f
--- /dev/null
+++ b/sourcemod/scripting/gokz-core.sp
@@ -0,0 +1,543 @@
+#include <sourcemod>
+
+#include <clientprefs>
+#include <cstrike>
+#include <dhooks>
+#include <regex>
+#include <sdkhooks>
+#include <sdktools>
+
+#include <gokz/core>
+#include <movementapi>
+
+#include <autoexecconfig>
+#include <sourcemod-colors>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#include <gokz/kzplayer>
+#include <gokz/jumpstats>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Core",
+ author = "DanZay",
+ description = "Core plugin of the GOKZ plugin set",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-core.txt"
+
+Handle gH_ThisPlugin;
+Handle gH_DHooks_OnTeleport;
+Handle gH_DHooks_SetModel;
+
+int gI_CmdNum[MAXPLAYERS + 1];
+int gI_TickCount[MAXPLAYERS + 1];
+bool gB_OldOnGround[MAXPLAYERS + 1];
+int gI_OldButtons[MAXPLAYERS + 1];
+int gI_TeleportCmdNum[MAXPLAYERS + 1];
+bool gB_OriginTeleported[MAXPLAYERS + 1];
+bool gB_VelocityTeleported[MAXPLAYERS + 1];
+bool gB_LateLoad;
+
+ConVar gCV_gokz_chat_prefix;
+ConVar gCV_sv_full_alltalk;
+
+#include "gokz-core/commands.sp"
+#include "gokz-core/modes.sp"
+#include "gokz-core/misc.sp"
+#include "gokz-core/options.sp"
+#include "gokz-core/teleports.sp"
+#include "gokz-core/triggerfix.sp"
+#include "gokz-core/demofix.sp"
+#include "gokz-core/teamnumfix.sp"
+
+#include "gokz-core/map/buttons.sp"
+#include "gokz-core/map/triggers.sp"
+#include "gokz-core/map/mapfile.sp"
+#include "gokz-core/map/prefix.sp"
+#include "gokz-core/map/starts.sp"
+#include "gokz-core/map/zones.sp"
+#include "gokz-core/map/end.sp"
+
+#include "gokz-core/menus/mode_menu.sp"
+#include "gokz-core/menus/options_menu.sp"
+
+#include "gokz-core/timer/pause.sp"
+#include "gokz-core/timer/timer.sp"
+#include "gokz-core/timer/virtual_buttons.sp"
+
+#include "gokz-core/forwards.sp"
+#include "gokz-core/natives.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ if (GetEngineVersion() != Engine_CSGO)
+ {
+ SetFailState("GOKZ only supports CS:GO servers.");
+ }
+
+ gH_ThisPlugin = myself;
+ gB_LateLoad = late;
+
+ CreateNatives();
+ RegPluginLibrary("gokz-core");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("common.phrases");
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-core.phrases");
+
+ CreateGlobalForwards();
+ CreateConVars();
+ HookEvents();
+ RegisterCommands();
+
+ OnPluginStart_MapTriggers();
+ OnPluginStart_MapButtons();
+ OnPluginStart_MapStarts();
+ OnPluginStart_MapEnd();
+ OnPluginStart_MapZones();
+ OnPluginStart_Options();
+ OnPluginStart_Triggerfix();
+ OnPluginStart_Demofix();
+ OnPluginStart_MapFile();
+ OnPluginStart_TeamNumber();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ OnAllPluginsLoaded_Modes();
+ OnAllPluginsLoaded_OptionsMenu();
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ OnClientPutInServer(client);
+ }
+ if (AreClientCookiesCached(client))
+ {
+ OnClientCookiesCached(client);
+ }
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ OnClientPutInServer_Timer(client);
+ OnClientPutInServer_Pause(client);
+ OnClientPutInServer_Teleports(client);
+ OnClientPutInServer_JoinTeam(client);
+ OnClientPutInServer_FirstSpawn(client);
+ OnClientPutInServer_VirtualButtons(client);
+ OnClientPutInServer_Options(client);
+ OnClientPutInServer_MapTriggers(client);
+ OnClientPutInServer_Triggerfix(client);
+ OnClientPutInServer_Noclip(client);
+ OnClientPutInServer_Turnbinds(client);
+ HookClientEvents(client);
+}
+
+public void OnClientDisconnect(int client)
+{
+ OnClientDisconnect_Timer(client);
+ OnClientDisconnect_ValidJump(client);
+}
+
+public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2])
+{
+ gI_CmdNum[client] = cmdnum;
+ gI_TickCount[client] = tickcount;
+ OnPlayerRunCmd_Triggerfix(client);
+ OnPlayerRunCmd_MapTriggers(client, buttons);
+ OnPlayerRunCmd_Turnbinds(client, buttons, tickcount, angles);
+ return Plugin_Continue;
+}
+
+public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2])
+{
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ OnPlayerRunCmdPost_VirtualButtons(client, buttons, cmdnum); // Emulate buttons first
+ OnPlayerRunCmdPost_Timer(client); // This should be first after emulating buttons
+ OnPlayerRunCmdPost_ValidJump(client);
+ UpdateTrackingVariables(client, cmdnum, buttons); // This should be last
+}
+
+public void OnClientCookiesCached(int client)
+{
+ OnClientCookiesCached_Options(client);
+}
+
+public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ OnPlayerSpawn_MapTriggers(client);
+ OnPlayerSpawn_Modes(client);
+ OnPlayerSpawn_Pause(client);
+ OnPlayerSpawn_ValidJump(client);
+ OnPlayerSpawn_FirstSpawn(client);
+ OnPlayerSpawn_GodMode(client);
+ OnPlayerSpawn_PlayerCollision(client);
+ }
+}
+
+public Action OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast)
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ OnPlayerJoinTeam_TeamNumber(event, client);
+ }
+ return Plugin_Continue;
+}
+
+public Action OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) // player_death pre hook
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ OnPlayerDeath_Timer(client);
+ OnPlayerDeath_ValidJump(client);
+ OnPlayerDeath_TeamNumber(client);
+ }
+ return Plugin_Continue;
+}
+
+public void OnPlayerJump(Event event, const char[] name, bool dontBroadcast)
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ OnPlayerJump_Triggers(client);
+}
+
+public MRESReturn DHooks_OnTeleport(int client, Handle params)
+{
+ gB_OriginTeleported[client] = !DHookIsNullParam(params, 1); // Origin affected
+ gB_VelocityTeleported[client] = !DHookIsNullParam(params, 3); // Velocity affected
+ OnTeleport_ValidJump(client);
+ OnTeleport_DelayVirtualButtons(client);
+ return MRES_Ignored;
+}
+
+public MRESReturn DHooks_OnSetModel(int client, Handle params)
+{
+ OnSetModel_PlayerCollision(client);
+ return MRES_Handled;
+}
+
+public void OnCSPlayerSpawnPost(int client)
+{
+ if (GetEntPropEnt(client, Prop_Send, "m_hGroundEntity") == -1)
+ {
+ SetEntityFlags(client, GetEntityFlags(client) & ~FL_ONGROUND);
+ }
+}
+
+public void OnClientPreThinkPost(int client)
+{
+ OnClientPreThinkPost_UseButtons(client);
+}
+
+public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype)
+{
+ OnChangeMovetype_Timer(client, newMovetype);
+ OnChangeMovetype_Pause(client, newMovetype);
+ OnChangeMovetype_ValidJump(client, oldMovetype, newMovetype);
+ OnChangeMovetype_MapTriggers(client, newMovetype);
+}
+
+public void Movement_OnStartTouchGround(int client)
+{
+ OnStartTouchGround_MapZones(client);
+ OnStartTouchGround_MapTriggers(client);
+}
+
+public void Movement_OnStopTouchGround(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ OnStopTouchGround_ValidJump(client, jumped, ladderJump, jumpbug);
+ OnStopTouchGround_MapTriggers(client);
+}
+
+public void GOKZ_OnTimerStart_Post(int client, int course)
+{
+ OnTimerStart_JoinTeam(client);
+ OnTimerStart_Pause(client);
+ OnTimerStart_Teleports(client);
+}
+
+public void GOKZ_OnTeleportToStart_Post(int client)
+{
+ OnTeleportToStart_Timer(client);
+}
+
+public void GOKZ_OnCountedTeleport_Post(int client)
+{
+ OnCountedTeleport_VirtualButtons(client);
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ Option coreOption;
+ if (!GOKZ_IsCoreOption(option, coreOption))
+ {
+ return;
+ }
+
+ OnOptionChanged_Options(client, coreOption, newValue);
+ OnOptionChanged_Timer(client, coreOption);
+ OnOptionChanged_Mode(client, coreOption);
+}
+
+public void GOKZ_OnJoinTeam(int client, int team)
+{
+ OnJoinTeam_Pause(client, team);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ OnMapStart_MapTriggers();
+ OnMapStart_KZConfig();
+ OnMapStart_Options();
+ OnMapStart_Prefix();
+ OnMapStart_CourseRegister();
+ OnMapStart_MapStarts();
+ OnMapStart_MapEnd();
+ OnMapStart_VirtualButtons();
+ OnMapStart_FixMissingSpawns();
+ OnMapStart_Checkpoints();
+ OnMapStart_TeamNumber();
+ OnMapStart_Demofix();
+}
+
+public void OnMapEnd()
+{
+ OnMapEnd_Demofix();
+}
+
+public void OnGameFrame()
+{
+ OnGameFrame_TeamNumber();
+ OnGameFrame_Triggerfix();
+}
+
+public void OnConfigsExecuted()
+{
+ OnConfigsExecuted_TimeLimit();
+ OnConfigsExecuted_OptionsMenu();
+}
+
+public Action OnNormalSound(int clients[MAXPLAYERS], int &numClients, char sample[PLATFORM_MAX_PATH], int &entity, int &channel, float &volume, int &level, int &pitch, int &flags, char soundEntry[PLATFORM_MAX_PATH], int &seed)
+{
+ if (OnNormalSound_StopSounds(entity) == Plugin_Handled)
+ {
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+public void OnEntityCreated(int entity, const char[] classname)
+{
+ // Don't react to player related entities
+ if (StrEqual(classname, "predicted_viewmodel") || StrEqual(classname, "item_assaultsuit")
+ || StrEqual(classname, "cs_bot") || StrEqual(classname, "player")
+ || StrContains(classname, "weapon") != -1)
+ {
+ return;
+ }
+ SDKHook(entity, SDKHook_Spawn, OnEntitySpawned);
+ SDKHook(entity, SDKHook_SpawnPost, OnEntitySpawnedPost);
+ OnEntityCreated_Triggerfix(entity, classname);
+}
+
+public void OnEntitySpawned(int entity)
+{
+ OnEntitySpawned_MapTriggers(entity);
+ OnEntitySpawned_MapButtons(entity);
+ OnEntitySpawned_MapStarts(entity);
+ OnEntitySpawned_MapZones(entity);
+}
+
+public void OnEntitySpawnedPost(int entity)
+{
+ OnEntitySpawnedPost_MapStarts(entity);
+ OnEntitySpawnedPost_MapEnd(entity);
+}
+
+public void OnClientConnected(int client)
+{
+ OnClientConnected_Triggerfix(client);
+}
+
+public void OnRoundStart(Event event, const char[] name, bool dontBroadcast) // round_start post no copy hook
+{
+ if (event == INVALID_HANDLE)
+ {
+ OnRoundStart_Timer();
+ OnRoundStart_ForceAllTalk();
+ OnRoundStart_Demofix();
+ return;
+ }
+ else
+ {
+ char objective[64];
+ event.GetString("objective", objective, sizeof(objective));
+ /*
+ External plugins that record GOTV demos can call round_start event to fix demo corruption,
+ which happens to stop the players' timer. GOKZ should only react on real round start events only.
+ */
+ if (IsRealObjective(objective))
+ {
+ OnRoundStart_Timer();
+ OnRoundStart_ForceAllTalk();
+ OnRoundStart_Demofix();
+ }
+ }
+}
+
+public Action CS_OnTerminateRound(float &delay, CSRoundEndReason &reason)
+{
+ return Plugin_Handled;
+}
+
+public void GOKZ_OnModeUnloaded(int mode)
+{
+ OnModeUnloaded_Options(mode);
+}
+
+public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ OnOptionsMenuCreated_OptionsMenu();
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_OptionsMenu();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CreateConVars()
+{
+ AutoExecConfig_SetFile("gokz-core", "sourcemod/gokz");
+ AutoExecConfig_SetCreateFile(true);
+
+ gCV_gokz_chat_prefix = AutoExecConfig_CreateConVar("gokz_chat_prefix", "{green}KZ {grey}| ", "Chat prefix used for GOKZ messages.");
+
+ AutoExecConfig_ExecuteFile();
+ AutoExecConfig_CleanFile();
+
+ gCV_sv_full_alltalk = FindConVar("sv_full_alltalk");
+
+ // Remove unwanted flags from constantly changed mode convars - replication is done manually in mode plugins
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ FindConVar(gC_ModeCVars[i]).Flags &= ~FCVAR_NOTIFY;
+ FindConVar(gC_ModeCVars[i]).Flags &= ~FCVAR_REPLICATED;
+ }
+}
+
+static void HookEvents()
+{
+ AddCommandsListeners();
+
+ HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post);
+ HookEvent("player_team", OnPlayerJoinTeam, EventHookMode_Pre);
+ HookEvent("player_death", OnPlayerDeath, EventHookMode_Pre);
+ HookEvent("player_jump", OnPlayerJump);
+ HookEvent("round_start", OnRoundStart, EventHookMode_PostNoCopy);
+ AddNormalSoundHook(OnNormalSound);
+
+ GameData gameData = new GameData("sdktools.games");
+ int offset;
+
+ // Setup DHooks OnTeleport for players
+ offset = gameData.GetOffset("Teleport");
+ gH_DHooks_OnTeleport = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnTeleport);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_ObjectPtr);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_VectorPtr);
+ DHookAddParam(gH_DHooks_OnTeleport, HookParamType_Bool);
+
+ gameData = new GameData("sdktools.games/engine.csgo");
+ offset = gameData.GetOffset("SetEntityModel");
+ gH_DHooks_SetModel = DHookCreate(offset, HookType_Entity, ReturnType_Void, ThisPointer_CBaseEntity, DHooks_OnSetModel);
+ DHookAddParam(gH_DHooks_SetModel, HookParamType_CharPtr);
+
+ delete gameData;
+}
+
+static void HookClientEvents(int client)
+{
+ DHookEntity(gH_DHooks_OnTeleport, true, client);
+ DHookEntity(gH_DHooks_SetModel, true, client);
+ SDKHook(client, SDKHook_SpawnPost, OnCSPlayerSpawnPost);
+ SDKHook(client, SDKHook_PreThinkPost, OnClientPreThinkPost);
+}
+
+static void UpdateTrackingVariables(int client, int cmdnum, int buttons)
+{
+ if (IsPlayerAlive(client))
+ {
+ gB_OldOnGround[client] = Movement_GetOnGround(client);
+ }
+
+ gI_OldButtons[client] = buttons;
+
+ if (gB_OriginTeleported[client] || gB_VelocityTeleported[client])
+ {
+ gI_TeleportCmdNum[client] = cmdnum;
+ }
+ gB_OriginTeleported[client] = false;
+ gB_VelocityTeleported[client] = false;
+}
+
+static bool IsRealObjective(char[] objective)
+{
+ return StrEqual(objective, "PRISON ESCAPE") || StrEqual(objective, "DEATHMATCH")
+ || StrEqual(objective, "BOMB TARGET") || StrEqual(objective, "HOSTAGE RESCUE");
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/commands.sp b/sourcemod/scripting/gokz-core/commands.sp
new file mode 100644
index 0000000..6aba82c
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/commands.sp
@@ -0,0 +1,385 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_options", CommandOptions, "[KZ] Open the options menu.");
+ RegConsoleCmd("sm_o", CommandOptions, "[KZ] Open the options menu.");
+ RegConsoleCmd("sm_checkpoint", CommandMakeCheckpoint, "[KZ] Set a checkpoint.");
+ RegConsoleCmd("sm_gocheck", CommandTeleportToCheckpoint, "[KZ] Teleport to your current checkpoint.");
+ RegConsoleCmd("sm_prev", CommandPrevCheckpoint, "[KZ] Go back a checkpoint.");
+ RegConsoleCmd("sm_next", CommandNextCheckpoint, "[KZ] Go forward a checkpoint.");
+ RegConsoleCmd("sm_undo", CommandUndoTeleport, "[KZ] Undo teleport.");
+ RegConsoleCmd("sm_start", CommandTeleportToStart, "[KZ] Teleport to the start.");
+ RegConsoleCmd("sm_searchstart", CommandSearchStart, "[KZ] Teleport to the start zone/button of a specified course.");
+ RegConsoleCmd("sm_end", CommandTeleportToEnd, "[KZ] Teleport to the end.");
+ RegConsoleCmd("sm_restart", CommandTeleportToStart, "[KZ] Teleport to your start position.");
+ RegConsoleCmd("sm_r", CommandTeleportToStart, "[KZ] Teleport to your start position.");
+ RegConsoleCmd("sm_setstartpos", CommandSetStartPos, "[KZ] Set your custom start position to your current position.");
+ RegConsoleCmd("sm_ssp", CommandSetStartPos, "[KZ] Set your custom start position to your current position.");
+ RegConsoleCmd("sm_clearstartpos", CommandClearStartPos, "[KZ] Clear your custom start position.");
+ RegConsoleCmd("sm_csp", CommandClearStartPos, "[KZ] Clear your custom start position.");
+ RegConsoleCmd("sm_main", CommandMain, "[KZ] Teleport to the start of the main course.");
+ RegConsoleCmd("sm_m", CommandMain, "[KZ] Teleport to the start of the main course.");
+ RegConsoleCmd("sm_bonus", CommandBonus, "[KZ] Teleport to the start of a bonus. Usage: `!bonus <#bonus>");
+ RegConsoleCmd("sm_b", CommandBonus, "[KZ] Teleport to the start of a bonus. Usage: `!b <#bonus>");
+ RegConsoleCmd("sm_pause", CommandTogglePause, "[KZ] Toggle pausing your timer and stopping you in your position.");
+ RegConsoleCmd("sm_resume", CommandTogglePause, "[KZ] Toggle pausing your timer and stopping you in your position.");
+ RegConsoleCmd("sm_stop", CommandStopTimer, "[KZ] Stop your timer.");
+ RegConsoleCmd("sm_virtualbuttonindicators", CommandToggleVirtualButtonIndicators, "[KZ] Toggle virtual button indicators.");
+ RegConsoleCmd("sm_vbi", CommandToggleVirtualButtonIndicators, "[KZ] Toggle virtual button indicators.");
+ RegConsoleCmd("sm_virtualbuttons", CommandToggleVirtualButtonsLock, "[KZ] Toggle locking virtual buttons, preventing them from being moved.");
+ RegConsoleCmd("sm_vb", CommandToggleVirtualButtonsLock, "[KZ] Toggle locking virtual buttons, preventing them from being moved.");
+ RegConsoleCmd("sm_mode", CommandMode, "[KZ] Open the movement mode selection menu.");
+ RegConsoleCmd("sm_vanilla", CommandVanilla, "[KZ] Switch to the Vanilla mode.");
+ RegConsoleCmd("sm_vnl", CommandVanilla, "[KZ] Switch to the Vanilla mode.");
+ RegConsoleCmd("sm_v", CommandVanilla, "[KZ] Switch to the Vanilla mode.");
+ RegConsoleCmd("sm_simplekz", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode.");
+ RegConsoleCmd("sm_skz", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode.");
+ RegConsoleCmd("sm_s", CommandSimpleKZ, "[KZ] Switch to the SimpleKZ mode.");
+ RegConsoleCmd("sm_kztimer", CommandKZTimer, "[KZ] Switch to the KZTimer mode.");
+ RegConsoleCmd("sm_kzt", CommandKZTimer, "[KZ] Switch to the KZTimer mode.");
+ RegConsoleCmd("sm_k", CommandKZTimer, "[KZ] Switch to the KZTimer mode.");
+ RegConsoleCmd("sm_nc", CommandToggleNoclip, "[KZ] Toggle noclip.");
+ RegConsoleCmd("+noclip", CommandEnableNoclip, "[KZ] Noclip on.");
+ RegConsoleCmd("-noclip", CommandDisableNoclip, "[KZ] Noclip off.");
+ RegConsoleCmd("sm_ncnt", CommandToggleNoclipNotrigger, "[KZ] Toggle noclip-notrigger.");
+ RegConsoleCmd("+noclipnt", CommandEnableNoclipNotrigger, "[KZ] Noclip-notrigger on.");
+ RegConsoleCmd("-noclipnt", CommandDisableNoclipNotrigger, "[KZ] Noclip-notrigger off.");
+ RegConsoleCmd("sm_sg", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard.");
+ RegConsoleCmd("sm_safe", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard.");
+ RegConsoleCmd("sm_safeguard", CommandNubSafeGuard, "[KZ] Toggle NUB safeguard.");
+ RegConsoleCmd("sm_pro", CommandProSafeGuard, "[KZ] Toggle PRO safeguard.");
+ RegConsoleCmd("kill", CommandKill);
+ RegConsoleCmd("killvector", CommandKill);
+ RegConsoleCmd("explode", CommandKill);
+ RegConsoleCmd("explodevector", CommandKill);
+}
+
+void AddCommandsListeners()
+{
+ AddCommandListener(CommandJoinTeam, "jointeam");
+}
+
+bool SwitchToModeIfAvailable(int client, int mode)
+{
+ if (!GOKZ_GetModeLoaded(mode))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Mode Not Available", gC_ModeNames[mode]);
+ return false;
+ }
+ else
+ {
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+ GOKZ_SetCoreOption(client, Option_Mode, mode);
+ return true;
+ }
+}
+
+public Action CommandKill(int client, int args)
+{
+ if (IsPlayerAlive(client) && GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+public Action CommandOptions(int client, int args)
+{
+ DisplayOptionsMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandJoinTeam(int client, const char[] command, int argc)
+{
+ char teamString[4];
+ GetCmdArgString(teamString, sizeof(teamString));
+ int team = StringToInt(teamString);
+
+ if (team == CS_TEAM_SPECTATOR)
+ {
+ if (!GOKZ_GetPaused(client) && !GOKZ_GetCanPause(client))
+ {
+ SendFakeTeamEvent(client);
+ return Plugin_Handled;
+ }
+ }
+ else if (IsPlayerAlive(client) && GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ SendFakeTeamEvent(client);
+ return Plugin_Handled;
+ }
+ GOKZ_JoinTeam(client, team);
+ return Plugin_Handled;
+}
+
+public Action CommandMakeCheckpoint(int client, int args)
+{
+ GOKZ_MakeCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandTeleportToCheckpoint(int client, int args)
+{
+ GOKZ_TeleportToCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandPrevCheckpoint(int client, int args)
+{
+ GOKZ_PrevCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandNextCheckpoint(int client, int args)
+{
+ GOKZ_NextCheckpoint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandUndoTeleport(int client, int args)
+{
+ GOKZ_UndoTeleport(client);
+ return Plugin_Handled;
+}
+
+public Action CommandTeleportToStart(int client, int args)
+{
+ GOKZ_TeleportToStart(client);
+ return Plugin_Handled;
+}
+
+public Action CommandSearchStart(int client, int args)
+{
+ if (args == 0)
+ {
+ GOKZ_TeleportToSearchStart(client, GetCurrentCourse(client));
+ return Plugin_Handled;
+ }
+ else
+ {
+ char argCourse[4];
+ GetCmdArg(1, argCourse, sizeof(argCourse));
+ int course = StringToInt(argCourse);
+ if (GOKZ_IsValidCourse(course, false))
+ {
+ GOKZ_TeleportToSearchStart(client, course);
+ }
+ else if (StrEqual(argCourse, "main", false) || course == 0)
+ {
+ GOKZ_TeleportToSearchStart(client, 0);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Course Number", argCourse);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandTeleportToEnd(int client, int args)
+{
+ if (args == 0)
+ {
+ GOKZ_TeleportToEnd(client, GetCurrentCourse(client));
+ }
+ else
+ {
+ char argCourse[4];
+ GetCmdArg(1, argCourse, sizeof(argCourse));
+ int course = StringToInt(argCourse);
+ if (GOKZ_IsValidCourse(course, false))
+ {
+ GOKZ_TeleportToEnd(client, course);
+ }
+ else if (StrEqual(argCourse, "main", false) || course == 0)
+ {
+ GOKZ_TeleportToEnd(client, 0);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Course Number", argCourse);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandSetStartPos(int client, int args)
+{
+ SetStartPositionToCurrent(client, StartPositionType_Custom);
+
+ GOKZ_PrintToChat(client, true, "%t", "Set Custom Start Position");
+ if (GOKZ_GetCoreOption(client, Option_CheckpointSounds) == CheckpointSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_CHECKPOINT, _, "Set Start Position");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action CommandClearStartPos(int client, int args)
+{
+ if (ClearCustomStartPosition(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Cleared Custom Start Position");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action CommandMain(int client, int args)
+{
+ TeleportToCourseStart(client, 0);
+ return Plugin_Handled;
+}
+
+public Action CommandBonus(int client, int args)
+{
+ if (args == 0)
+ { // Go to Bonus 1
+ TeleportToCourseStart(client, 1);
+ }
+ else
+ { // Go to specified Bonus #
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ TeleportToCourseStart(client, bonus);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandTogglePause(int client, int args)
+{
+ if (!IsPlayerAlive(client))
+ {
+ GOKZ_RespawnPlayer(client);
+ }
+ else
+ {
+ TogglePause(client);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandStopTimer(int client, int args)
+{
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped");
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleVirtualButtonIndicators(int client, int args)
+{
+ if (GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled)
+ {
+ GOKZ_SetCoreOption(client, Option_VirtualButtonIndicators, VirtualButtonIndicators_Enabled);
+ }
+ else
+ {
+ GOKZ_SetCoreOption(client, Option_VirtualButtonIndicators, VirtualButtonIndicators_Disabled);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleVirtualButtonsLock(int client, int args)
+{
+ if (ToggleVirtualButtonsLock(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Locked Virtual Buttons");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Unlocked Virtual Buttons");
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandMode(int client, int args)
+{
+ DisplayModeMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandVanilla(int client, int args)
+{
+ SwitchToModeIfAvailable(client, Mode_Vanilla);
+ return Plugin_Handled;
+}
+
+public Action CommandSimpleKZ(int client, int args)
+{
+ SwitchToModeIfAvailable(client, Mode_SimpleKZ);
+ return Plugin_Handled;
+}
+
+public Action CommandKZTimer(int client, int args)
+{
+ SwitchToModeIfAvailable(client, Mode_KZTimer);
+ return Plugin_Handled;
+}
+
+public Action CommandToggleNoclip(int client, int args)
+{
+ ToggleNoclip(client);
+ return Plugin_Handled;
+}
+
+public Action CommandEnableNoclip(int client, int args)
+{
+ EnableNoclip(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDisableNoclip(int client, int args)
+{
+ DisableNoclip(client);
+ return Plugin_Handled;
+}
+
+public Action CommandToggleNoclipNotrigger(int client, int args)
+{
+ ToggleNoclipNotrigger(client);
+ return Plugin_Handled;
+}
+
+public Action CommandEnableNoclipNotrigger(int client, int args)
+{
+ EnableNoclipNotrigger(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDisableNoclipNotrigger(int client, int args)
+{
+ DisableNoclipNotrigger(client);
+ return Plugin_Handled;
+}
+
+public Action CommandNubSafeGuard(int client, int args)
+{
+ ToggleNubSafeGuard(client);
+ return Plugin_Handled;
+}
+
+public Action CommandProSafeGuard(int client, int args)
+{
+ ToggleProSafeGuard(client);
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/demofix.sp b/sourcemod/scripting/gokz-core/demofix.sp
new file mode 100644
index 0000000..84a9307
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/demofix.sp
@@ -0,0 +1,110 @@
+static ConVar CV_EnableDemofix;
+static Handle H_DemofixTimer;
+static bool mapRunning;
+
+void OnPluginStart_Demofix()
+{
+ AddCommandListener(Command_Demorestart, "demorestart");
+ CV_EnableDemofix = AutoExecConfig_CreateConVar("gokz_demofix", "1", "Whether GOKZ applies demo record fix to server. (0 = Disabled, 1 = Update warmup period once, 2 = Regularly reset warmup period)", _, true, 0.0, true, 2.0);
+ CV_EnableDemofix.AddChangeHook(OnDemofixConVarChanged);
+ // If the map is tweaking the warmup value, we need to rerun the fix again.
+ FindConVar("mp_warmuptime").AddChangeHook(OnDemofixConVarChanged);
+ // We assume that the map is already loaded on late load.
+ if (gB_LateLoad)
+ {
+ mapRunning = true;
+ }
+}
+
+void OnMapStart_Demofix()
+{
+ mapRunning = true;
+}
+
+void OnMapEnd_Demofix()
+{
+ mapRunning = false;
+}
+
+void OnRoundStart_Demofix()
+{
+ DoDemoFix();
+}
+
+public Action Command_Demorestart(int client, const char[] command, int argc)
+{
+ FixRecord(client);
+ return Plugin_Continue;
+}
+
+static void FixRecord(int client)
+{
+ // For some reasons, demo playback speed is absolute trash without a round_start event.
+ // So whenever the client starts recording a demo, we create the event and send it to them.
+ Event e = CreateEvent("round_start", true);
+ int timelimit = FindConVar("mp_timelimit").IntValue;
+ e.SetInt("timelimit", timelimit);
+ e.SetInt("fraglimit", 0);
+ e.SetString("objective", "demofix");
+
+ e.FireToClient(client);
+ delete e;
+}
+
+public void OnDemofixConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ DoDemoFix();
+}
+
+public Action Timer_EnableDemoRecord(Handle timer)
+{
+ EnableDemoRecord();
+ return Plugin_Continue;
+}
+
+static void DoDemoFix()
+{
+ if (H_DemofixTimer != null)
+ {
+ delete H_DemofixTimer;
+ }
+ // Setting the cvar value to 1 can avoid clogging the demo file and slightly increase performance.
+ switch (CV_EnableDemofix.IntValue)
+ {
+ case 0:
+ {
+ if (!mapRunning)
+ {
+ return;
+ }
+
+ GameRules_SetProp("m_bWarmupPeriod", 0);
+ }
+ case 1:
+ {
+ // Set warmup time to 2^31-1, effectively forever
+ if (FindConVar("mp_warmuptime").IntValue != 2147483647)
+ {
+ FindConVar("mp_warmuptime").SetInt(2147483647);
+ }
+ EnableDemoRecord();
+ }
+ case 2:
+ {
+ H_DemofixTimer = CreateTimer(1.0, Timer_EnableDemoRecord, _, TIMER_REPEAT);
+ }
+ }
+}
+
+static void EnableDemoRecord()
+{
+ // Enable warmup to allow demo recording
+ // m_fWarmupPeriodEnd is set in the past to hide the timer UI
+ if (!mapRunning)
+ {
+ return;
+ }
+ GameRules_SetProp("m_bWarmupPeriod", 1);
+ GameRules_SetPropFloat("m_fWarmupPeriodStart", GetGameTime() - 1.0);
+ GameRules_SetPropFloat("m_fWarmupPeriodEnd", GetGameTime() - 1.0);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/forwards.sp b/sourcemod/scripting/gokz-core/forwards.sp
new file mode 100644
index 0000000..efb064f
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/forwards.sp
@@ -0,0 +1,401 @@
+static GlobalForward H_OnOptionsLoaded;
+static GlobalForward H_OnOptionChanged;
+static GlobalForward H_OnTimerStart;
+static GlobalForward H_OnTimerStart_Post;
+static GlobalForward H_OnTimerEnd;
+static GlobalForward H_OnTimerEnd_Post;
+static GlobalForward H_OnTimerEndMessage;
+static GlobalForward H_OnTimerStopped;
+static GlobalForward H_OnPause;
+static GlobalForward H_OnPause_Post;
+static GlobalForward H_OnResume;
+static GlobalForward H_OnResume_Post;
+static GlobalForward H_OnMakeCheckpoint;
+static GlobalForward H_OnMakeCheckpoint_Post;
+static GlobalForward H_OnTeleportToCheckpoint;
+static GlobalForward H_OnTeleportToCheckpoint_Post;
+static GlobalForward H_OnTeleport;
+static GlobalForward H_OnPrevCheckpoint;
+static GlobalForward H_OnPrevCheckpoint_Post;
+static GlobalForward H_OnNextCheckpoint;
+static GlobalForward H_OnNextCheckpoint_Post;
+static GlobalForward H_OnTeleportToStart;
+static GlobalForward H_OnTeleportToStart_Post;
+static GlobalForward H_OnTeleportToEnd;
+static GlobalForward H_OnTeleportToEnd_Post;
+static GlobalForward H_OnUndoTeleport;
+static GlobalForward H_OnUndoTeleport_Post;
+static GlobalForward H_OnCountedTeleport_Post;
+static GlobalForward H_OnStartPositionSet_Post;
+static GlobalForward H_OnJumpValidated;
+static GlobalForward H_OnJumpInvalidated;
+static GlobalForward H_OnJoinTeam;
+static GlobalForward H_OnFirstSpawn;
+static GlobalForward H_OnModeLoaded;
+static GlobalForward H_OnModeUnloaded;
+static GlobalForward H_OnTimerNativeCalledExternally;
+static GlobalForward H_OnOptionsMenuCreated;
+static GlobalForward H_OnOptionsMenuReady;
+static GlobalForward H_OnCourseRegistered;
+static GlobalForward H_OnRunInvalidated;
+static GlobalForward H_OnEmitSoundToClient;
+
+void CreateGlobalForwards()
+{
+ H_OnOptionsLoaded = new GlobalForward("GOKZ_OnOptionsLoaded", ET_Ignore, Param_Cell);
+ H_OnOptionChanged = new GlobalForward("GOKZ_OnOptionChanged", ET_Ignore, Param_Cell, Param_String, Param_Cell);
+ H_OnTimerStart = new GlobalForward("GOKZ_OnTimerStart", ET_Hook, Param_Cell, Param_Cell);
+ H_OnTimerStart_Post = new GlobalForward("GOKZ_OnTimerStart_Post", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnTimerEnd = new GlobalForward("GOKZ_OnTimerEnd", ET_Hook, Param_Cell, Param_Cell, Param_Float, Param_Cell);
+ H_OnTimerEnd_Post = new GlobalForward("GOKZ_OnTimerEnd_Post", ET_Ignore, Param_Cell, Param_Cell, Param_Float, Param_Cell);
+ H_OnTimerEndMessage = new GlobalForward("GOKZ_OnTimerEndMessage", ET_Hook, Param_Cell, Param_Cell, Param_Float, Param_Cell);
+ H_OnTimerStopped = new GlobalForward("GOKZ_OnTimerStopped", ET_Ignore, Param_Cell);
+ H_OnPause = new GlobalForward("GOKZ_OnPause", ET_Hook, Param_Cell);
+ H_OnPause_Post = new GlobalForward("GOKZ_OnPause_Post", ET_Ignore, Param_Cell);
+ H_OnResume = new GlobalForward("GOKZ_OnResume", ET_Hook, Param_Cell);
+ H_OnResume_Post = new GlobalForward("GOKZ_OnResume_Post", ET_Ignore, Param_Cell);
+ H_OnMakeCheckpoint = new GlobalForward("GOKZ_OnMakeCheckpoint", ET_Hook, Param_Cell);
+ H_OnMakeCheckpoint_Post = new GlobalForward("GOKZ_OnMakeCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnTeleportToCheckpoint = new GlobalForward("GOKZ_OnTeleportToCheckpoint", ET_Hook, Param_Cell);
+ H_OnTeleportToCheckpoint_Post = new GlobalForward("GOKZ_OnTeleportToCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnTeleport = new GlobalForward("GOKZ_OnTeleport", ET_Hook, Param_Cell);
+ H_OnPrevCheckpoint = new GlobalForward("GOKZ_OnPrevCheckpoint", ET_Hook, Param_Cell);
+ H_OnPrevCheckpoint_Post = new GlobalForward("GOKZ_OnPrevCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnNextCheckpoint = new GlobalForward("GOKZ_OnNextCheckpoint", ET_Hook, Param_Cell);
+ H_OnNextCheckpoint_Post = new GlobalForward("GOKZ_OnNextCheckpoint_Post", ET_Ignore, Param_Cell);
+ H_OnTeleportToStart = new GlobalForward("GOKZ_OnTeleportToStart", ET_Hook, Param_Cell, Param_Cell);
+ H_OnTeleportToStart_Post = new GlobalForward("GOKZ_OnTeleportToStart_Post", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnTeleportToEnd = new GlobalForward("GOKZ_OnTeleportToEnd", ET_Hook, Param_Cell, Param_Cell);
+ H_OnTeleportToEnd_Post = new GlobalForward("GOKZ_OnTeleportToEnd_Post", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnUndoTeleport = new GlobalForward("GOKZ_OnUndoTeleport", ET_Hook, Param_Cell);
+ H_OnUndoTeleport_Post = new GlobalForward("GOKZ_OnUndoTeleport_Post", ET_Ignore, Param_Cell);
+ H_OnStartPositionSet_Post = new GlobalForward("GOKZ_OnStartPositionSet_Post", ET_Ignore, Param_Cell, Param_Cell, Param_Array, Param_Array);
+ H_OnCountedTeleport_Post = new GlobalForward("GOKZ_OnCountedTeleport_Post", ET_Ignore, Param_Cell);
+ H_OnJumpValidated = new GlobalForward("GOKZ_OnJumpValidated", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+ H_OnJumpInvalidated = new GlobalForward("GOKZ_OnJumpInvalidated", ET_Ignore, Param_Cell);
+ H_OnJoinTeam = new GlobalForward("GOKZ_OnJoinTeam", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnFirstSpawn = new GlobalForward("GOKZ_OnFirstSpawn", ET_Ignore, Param_Cell);
+ H_OnModeLoaded = new GlobalForward("GOKZ_OnModeLoaded", ET_Ignore, Param_Cell);
+ H_OnModeUnloaded = new GlobalForward("GOKZ_OnModeUnloaded", ET_Ignore, Param_Cell);
+ H_OnTimerNativeCalledExternally = new GlobalForward("GOKZ_OnTimerNativeCalledExternally", ET_Event, Param_Cell, Param_Cell);
+ H_OnOptionsMenuCreated = new GlobalForward("GOKZ_OnOptionsMenuCreated", ET_Ignore, Param_Cell);
+ H_OnOptionsMenuReady = new GlobalForward("GOKZ_OnOptionsMenuReady", ET_Ignore, Param_Cell);
+ H_OnCourseRegistered = new GlobalForward("GOKZ_OnCourseRegistered", ET_Ignore, Param_Cell);
+ H_OnRunInvalidated = new GlobalForward("GOKZ_OnRunInvalidated", ET_Ignore, Param_Cell);
+ H_OnEmitSoundToClient = new GlobalForward("GOKZ_OnEmitSoundToClient", ET_Hook, Param_Cell, Param_String, Param_FloatByRef, Param_String);
+}
+
+void Call_GOKZ_OnOptionsLoaded(int client)
+{
+ Call_StartForward(H_OnOptionsLoaded);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnOptionChanged(int client, const char[] option, int optionValue)
+{
+ Call_StartForward(H_OnOptionChanged);
+ Call_PushCell(client);
+ Call_PushString(option);
+ Call_PushCell(optionValue);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerStart(int client, int course, Action &result)
+{
+ Call_StartForward(H_OnTimerStart);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTimerStart_Post(int client, int course)
+{
+ Call_StartForward(H_OnTimerStart_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerEnd(int client, int course, float time, int teleportsUsed, Action &result)
+{
+ Call_StartForward(H_OnTimerEnd);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ Call_StartForward(H_OnTimerEnd_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed, Action &result)
+{
+ Call_StartForward(H_OnTimerEndMessage);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTimerStopped(int client)
+{
+ Call_StartForward(H_OnTimerStopped);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnPause(int client, Action &result)
+{
+ Call_StartForward(H_OnPause);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnPause_Post(int client)
+{
+ Call_StartForward(H_OnPause_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnResume(int client, Action &result)
+{
+ Call_StartForward(H_OnResume);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnResume_Post(int client)
+{
+ Call_StartForward(H_OnResume_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnMakeCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnMakeCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnMakeCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnMakeCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleportToCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnTeleportToCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTeleportToCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnTeleportToCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleport(int client)
+{
+ Call_StartForward(H_OnTeleport);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnPrevCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnPrevCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnPrevCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnPrevCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnNextCheckpoint(int client, Action &result)
+{
+ Call_StartForward(H_OnNextCheckpoint);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnNextCheckpoint_Post(int client)
+{
+ Call_StartForward(H_OnNextCheckpoint_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleportToStart(int client, int course, Action &result)
+{
+ Call_StartForward(H_OnTeleportToStart);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTeleportToStart_Post(int client, int course)
+{
+ Call_StartForward(H_OnTeleportToStart_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTeleportToEnd(int client, int course, Action &result)
+{
+ Call_StartForward(H_OnTeleportToEnd);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnTeleportToEnd_Post(int client, int course)
+{
+ Call_StartForward(H_OnTeleportToEnd_Post);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnUndoTeleport(int client, Action &result)
+{
+ Call_StartForward(H_OnUndoTeleport);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnUndoTeleport_Post(int client)
+{
+ Call_StartForward(H_OnUndoTeleport_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnCountedTeleport_Post(int client)
+{
+ Call_StartForward(H_OnCountedTeleport_Post);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3])
+{
+ Call_StartForward(H_OnStartPositionSet_Post);
+ Call_PushCell(client);
+ Call_PushCell(type);
+ Call_PushArray(origin, 3);
+ Call_PushArray(angles, 3);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ Call_StartForward(H_OnJumpValidated);
+ Call_PushCell(client);
+ Call_PushCell(jumped);
+ Call_PushCell(ladderJump);
+ Call_PushCell(jumpbug);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnJumpInvalidated(int client)
+{
+ Call_StartForward(H_OnJumpInvalidated);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnJoinTeam(int client, int team)
+{
+ Call_StartForward(H_OnJoinTeam);
+ Call_PushCell(client);
+ Call_PushCell(team);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnFirstSpawn(int client)
+{
+ Call_StartForward(H_OnFirstSpawn);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnModeLoaded(int mode)
+{
+ Call_StartForward(H_OnModeLoaded);
+ Call_PushCell(mode);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnModeUnloaded(int mode)
+{
+ Call_StartForward(H_OnModeUnloaded);
+ Call_PushCell(mode);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client, Action &result)
+{
+ Call_StartForward(H_OnTimerNativeCalledExternally);
+ Call_PushCell(plugin);
+ Call_PushCell(client);
+ Call_Finish(result);
+}
+
+void Call_GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ Call_StartForward(H_OnOptionsMenuCreated);
+ Call_PushCell(topMenu);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ Call_StartForward(H_OnOptionsMenuReady);
+ Call_PushCell(topMenu);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnCourseRegistered(int course)
+{
+ Call_StartForward(H_OnCourseRegistered);
+ Call_PushCell(course);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnRunInvalidated(int client)
+{
+ Call_StartForward(H_OnRunInvalidated);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description, Action &result)
+{
+ Call_StartForward(H_OnEmitSoundToClient);
+ Call_PushCell(client);
+ Call_PushString(sample);
+ Call_PushFloatRef(volume);
+ Call_PushString(description);
+ Call_Finish(result);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/buttons.sp b/sourcemod/scripting/gokz-core/map/buttons.sp
new file mode 100644
index 0000000..8923fbd
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/buttons.sp
@@ -0,0 +1,138 @@
+/*
+ Hooks between specifically named func_buttons and GOKZ.
+*/
+
+
+
+static Regex RE_BonusStartButton;
+static Regex RE_BonusEndButton;
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapButtons()
+{
+ RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX);
+ RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX);
+}
+
+void OnEntitySpawned_MapButtons(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+ if (!StrEqual("func_button", buffer, false))
+ {
+ return;
+ }
+
+ if (GetEntityName(entity, buffer, sizeof(buffer)) == 0)
+ {
+ return;
+ }
+
+ int course = 0;
+ if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnStartButtonPress);
+ RegisterCourseStart(course);
+ }
+ else if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnEndButtonPress);
+ RegisterCourseEnd(course);
+ }
+ else if ((course = GetStartButtonBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnBonusStartButtonPress);
+ RegisterCourseStart(course);
+ }
+ else if ((course = GetEndButtonBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnPressed", OnBonusEndButtonPress);
+ RegisterCourseEnd(course);
+ }
+}
+
+public void OnStartButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessStartButtonPress(activator, 0);
+}
+
+public void OnEndButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessEndButtonPress(activator, 0);
+}
+
+public void OnBonusStartButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetStartButtonBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessStartButtonPress(activator, course);
+}
+
+public void OnBonusEndButtonPress(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetEndButtonBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessEndButtonPress(activator, course);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void ProcessStartButtonPress(int client, int course)
+{
+ if (GOKZ_StartTimer(client, course))
+ {
+ // Only calling on success is intended behaviour (and prevents virtual button exploits)
+ OnStartButtonPress_Teleports(client, course);
+ OnStartButtonPress_VirtualButtons(client, course);
+ }
+}
+
+static void ProcessEndButtonPress(int client, int course)
+{
+ GOKZ_EndTimer(client, course);
+ OnEndButtonPress_VirtualButtons(client, course);
+}
+
+static int GetStartButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1);
+}
+
+static int GetEndButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/end.sp b/sourcemod/scripting/gokz-core/map/end.sp
new file mode 100644
index 0000000..d119084
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/end.sp
@@ -0,0 +1,155 @@
+/*
+ Hooks between specifically named end destinations and GOKZ
+*/
+
+
+
+static Regex RE_BonusEndButton;
+static Regex RE_BonusEndZone;
+static CourseTimerType endType[GOKZ_MAX_COURSES];
+static float endOrigin[GOKZ_MAX_COURSES][3];
+static float endAngles[GOKZ_MAX_COURSES][3];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapEnd()
+{
+ RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX);
+ RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX);
+}
+
+void OnEntitySpawnedPost_MapEnd(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+
+ if (StrEqual("trigger_multiple", buffer, false))
+ {
+ bool isEndZone;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false))
+ {
+ isEndZone = true;
+ StoreEnd(0, entity, CourseTimerType_ZoneNew);
+ }
+ else if (GetEndZoneBonusNumber(entity) != -1)
+ {
+ int course = GetEndZoneBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isEndZone = true;
+ StoreEnd(course, entity, CourseTimerType_ZoneNew);
+ }
+ }
+ }
+ if (!isEndZone)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer)
+ {
+ StoreEnd(trigger.course, entity, CourseTimerType_ZoneLegacy);
+ }
+ }
+ }
+ else if (StrEqual("func_button", buffer, false))
+ {
+ bool isEndButton;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false))
+ {
+ isEndButton = true;
+ StoreEnd(0, entity, CourseTimerType_Button);
+ }
+ else
+ {
+ int course = GetEndButtonBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isEndButton = true;
+ StoreEnd(course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+ if (!isEndButton)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer)
+ {
+ StoreEnd(trigger.course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+}
+
+void OnMapStart_MapEnd()
+{
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ endType[course] = CourseTimerType_None;
+ }
+}
+
+bool GetMapEndPosition(int course, float origin[3], float angles[3])
+{
+ if (endType[course] == CourseTimerType_None)
+ {
+ return false;
+ }
+
+ origin = endOrigin[course];
+ angles = endAngles[course];
+
+ return true;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void StoreEnd(int course, int entity, CourseTimerType type)
+{
+ // If StoreEnd is called, then there is at least an end position (even though it might not be a valid one)
+ if (endType[course] < CourseTimerType_Default)
+ {
+ endType[course] = CourseTimerType_Default;
+ }
+
+ // Real zone is always better than "fake" zones which are better than buttons
+ // as the buttons found in a map with fake zones aren't meant to be visible.
+ if (endType[course] >= type)
+ {
+ return;
+ }
+
+ float origin[3], distFromCenter[3];
+ GetEntityPositions(entity, origin, endOrigin[course], endAngles[course], distFromCenter);
+
+ // If it is a button or the center of the center of the zone is invalid
+ if (type == CourseTimerType_Button || !IsSpawnValid(endOrigin[course]))
+ {
+ // Attempt with various positions around the entity, pick the first valid one.
+ if (!FindValidPositionAroundTimerEntity(entity, endOrigin[course], endAngles[course], type == CourseTimerType_Button))
+ {
+ endOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone.
+ return;
+ }
+ }
+
+ // Only update the CourseTimerType if a valid position is found.
+ endType[course] = type;
+}
+
+static int GetEndButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1);
+}
+
+static int GetEndZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1);
+}
diff --git a/sourcemod/scripting/gokz-core/map/mapfile.sp b/sourcemod/scripting/gokz-core/map/mapfile.sp
new file mode 100644
index 0000000..db60e7e
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/mapfile.sp
@@ -0,0 +1,502 @@
+/*
+ Mapping API
+
+ Reads data from the current map file.
+*/
+
+static Regex RE_BonusStartButton;
+static Regex RE_BonusEndButton;
+
+// NOTE: 4 megabyte array for entity lump reading.
+static char gEntityLump[4194304];
+
+// =====[ PUBLIC ]=====
+
+void EntlumpParse(StringMap antiBhopTriggers, StringMap teleportTriggers, StringMap timerButtonTriggers, int &mappingApiVersion)
+{
+ char mapPath[512];
+ GetCurrentMap(mapPath, sizeof(mapPath));
+ Format(mapPath, sizeof(mapPath), "maps/%s.bsp", mapPath);
+
+ // https://developer.valvesoftware.com/wiki/Source_BSP_File_Format
+
+ File file = OpenFile(mapPath, "rb");
+ if (file != INVALID_HANDLE)
+ {
+ int identifier;
+ file.ReadInt32(identifier);
+
+ if (identifier == GOKZ_BSP_HEADER_IDENTIFIER)
+ {
+ // skip version number
+ file.Seek(4, SEEK_CUR);
+
+ // the entity lump info is the first lump in the array, so we don't need to seek any further.
+ int offset;
+ int length;
+ file.ReadInt32(offset);
+ file.ReadInt32(length);
+
+ // jump to the start of the entity lump
+ file.Seek(offset, SEEK_SET);
+
+ int charactersRead = file.ReadString(gEntityLump, sizeof(gEntityLump), length);
+ delete file;
+ if (charactersRead >= sizeof(gEntityLump) - 1)
+ {
+ PushMappingApiError("ERROR: Entity lump: The map's entity lump is too big! Reduce the amount of entities in your map.");
+ return;
+ }
+ gEntityLump[length] = '\0';
+
+ int index = 0;
+
+ StringMap entity = new StringMap();
+ bool gotWorldSpawn = false;
+ while (EntlumpParseEntity(entity, gEntityLump, index))
+ {
+ char classname[128];
+ char targetName[GOKZ_ENTLUMP_MAX_VALUE];
+ entity.GetString("classname", classname, sizeof(classname));
+
+ if (!gotWorldSpawn && StrEqual("worldspawn", classname, false))
+ {
+ gotWorldSpawn = true;
+ char versionString[32];
+ if (entity.GetString("climb_mapping_api_version", versionString, sizeof(versionString)))
+ {
+ if (StringToIntEx(versionString, mappingApiVersion) == 0)
+ {
+ PushMappingApiError("ERROR: Entity lump: Couldn't parse Mapping API version from map properties: \"%s\".", versionString);
+ mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+ }
+ }
+ else
+ {
+ // map doesn't have a mapping api version.
+ mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+ }
+ }
+ else if (StrEqual("trigger_multiple", classname, false))
+ {
+ TriggerType triggerType;
+ if (!gotWorldSpawn || mappingApiVersion != GOKZ_MAPPING_API_VERSION_NONE)
+ {
+ if (entity.GetString("targetname", targetName, sizeof(targetName)))
+ {
+ // get trigger properties if applicable
+ triggerType = GetTriggerType(targetName);
+ if (triggerType == TriggerType_Antibhop)
+ {
+ AntiBhopTrigger trigger;
+ if (GetAntiBhopTriggerEntityProperties(trigger, entity))
+ {
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ antiBhopTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ }
+ else if (triggerType == TriggerType_Teleport)
+ {
+ TeleportTrigger trigger;
+ if (GetTeleportTriggerEntityProperties(trigger, entity))
+ {
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ teleportTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ }
+ }
+ }
+
+ // Tracking legacy timer triggers that press the timer buttons upon triggered.
+ if (triggerType == TriggerType_Invalid)
+ {
+ char touchOutput[128];
+ ArrayList value;
+
+ if (entity.GetString("OnStartTouch", touchOutput, sizeof(touchOutput)))
+ {
+ TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers);
+ }
+ else if (entity.GetValue("OnStartTouch", value)) // If there are multiple outputs, we have to check for all of them.
+ {
+ for (int i = 0; i < value.Length; i++)
+ {
+ value.GetString(i, touchOutput, sizeof(touchOutput));
+ TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers);
+ }
+ }
+ }
+ }
+ else if (StrEqual("func_button", classname, false))
+ {
+ char pressOutput[128];
+ ArrayList value;
+
+ if (entity.GetString("OnPressed", pressOutput, sizeof(pressOutput)))
+ {
+ TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers);
+ }
+ else if (entity.GetValue("OnPressed", value)) // If there are multiple outputs, we have to check for all of them.
+ {
+ for (int i = 0; i < value.Length; i++)
+ {
+ value.GetString(i, pressOutput, sizeof(pressOutput));
+ TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers);
+ }
+ }
+ }
+ // clear for next loop
+ entity.Clear();
+ }
+ delete entity;
+ }
+ delete file;
+ }
+ else
+ {
+ // TODO: do something more elegant
+ SetFailState("Catastrophic extreme hyperfailure! Mapping API Couldn't open the map file for reading! %s. The map file might be gone or another program is using it.", mapPath);
+ }
+}
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapFile()
+{
+ char buffer[64];
+ char press[8];
+ FormatEx(press, sizeof(press), "%s%s", CHAR_ESCAPE, "Press");
+
+ buffer = GOKZ_BONUS_START_BUTTON_NAME_REGEX;
+ ReplaceStringEx(buffer, sizeof(buffer), "$", "");
+ StrCat(buffer, sizeof(buffer), press);
+ RE_BonusStartButton = CompileRegex(buffer);
+
+ buffer = GOKZ_BONUS_END_BUTTON_NAME_REGEX;
+ ReplaceStringEx(buffer, sizeof(buffer), "$", "");
+ StrCat(buffer, sizeof(buffer), press);
+ RE_BonusEndButton = CompileRegex(buffer);
+}
+
+
+// =====[ PRIVATE ]=====
+
+static void EntlumpSkipAllWhiteSpace(char[] entityLump, int &index)
+{
+ while (IsCharSpace(entityLump[index]) && entityLump[index] != '\0')
+ {
+ index++;
+ }
+}
+
+static int EntlumpGetString(char[] result, int maxLength, int copyCount, char[] entityLump, int entlumpIndex)
+{
+ int finalLength;
+ for (int i = 0; i < maxLength - 1 && i < copyCount; i++)
+ {
+ if (entityLump[entlumpIndex + i] == '\0')
+ {
+ break;
+ }
+ result[i] = entityLump[entlumpIndex + i];
+ finalLength++;
+ }
+
+ result[finalLength] = '\0';
+ return finalLength;
+}
+
+static EntlumpToken EntlumpGetToken(char[] entityLump, int &entlumpIndex)
+{
+ EntlumpToken result;
+
+ EntlumpSkipAllWhiteSpace(entityLump, entlumpIndex);
+
+ switch (entityLump[entlumpIndex])
+ {
+ case '{':
+ {
+ result.type = EntlumpTokenType_OpenBrace;
+ EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex);
+ entlumpIndex++;
+ }
+ case '}':
+ {
+ result.type = EntlumpTokenType_CloseBrace;
+ EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex);
+ entlumpIndex++;
+ }
+ case '\0':
+ {
+ result.type = EntlumpTokenType_EndOfStream;
+ EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex);
+ entlumpIndex++;
+ }
+ case '\"':
+ {
+ result.type = EntlumpTokenType_Identifier;
+ int identifierLen;
+ entlumpIndex++;
+ for (int i = 0; i < sizeof(result.string) - 1; i++)
+ {
+ // NOTE: Unterminated strings can probably never happen, since the map has to be
+ // loaded by the game first and the engine will fail the load before we get to it.
+ if (entityLump[entlumpIndex + i] == '\0')
+ {
+ result.type = EntlumpTokenType_Unknown;
+ break;
+ }
+ if (entityLump[entlumpIndex + i] == '\"')
+ {
+ break;
+ }
+ result.string[i] = entityLump[entlumpIndex + i];
+ identifierLen++;
+ }
+
+ entlumpIndex += identifierLen + 1; // +1 to skip over last quotation mark
+ result.string[identifierLen] = '\0';
+ }
+ default:
+ {
+ result.type = EntlumpTokenType_Unknown;
+ result.string[0] = entityLump[entlumpIndex];
+ result.string[1] = '\0';
+ }
+ }
+
+ return result;
+}
+
+static bool EntlumpParseEntity(StringMap result, char[] entityLump, int &entlumpIndex)
+{
+ EntlumpToken token;
+ token = EntlumpGetToken(entityLump, entlumpIndex);
+ if (token.type == EntlumpTokenType_EndOfStream)
+ {
+ return false;
+ }
+
+ // NOTE: The following errors will very very likely never happen, since the entity lump has to be
+ // loaded by the game first and the engine will fail the load before we get to it.
+ // But if there's an obscure bug in this code, then we'll know!!!
+ for (;;)
+ {
+ token = EntlumpGetToken(entityLump, entlumpIndex);
+ switch (token.type)
+ {
+ case EntlumpTokenType_OpenBrace:
+ {
+ continue;
+ }
+ case EntlumpTokenType_Identifier:
+ {
+ EntlumpToken valueToken;
+ valueToken = EntlumpGetToken(entityLump, entlumpIndex);
+ if (valueToken.type == EntlumpTokenType_Identifier)
+ {
+ char tempString[GOKZ_ENTLUMP_MAX_VALUE];
+ ArrayList values;
+ if (result.GetString(token.string, tempString, sizeof(tempString)))
+ {
+ result.Remove(token.string);
+ values = new ArrayList(ByteCountToCells(GOKZ_ENTLUMP_MAX_VALUE));
+ values.PushString(tempString);
+ values.PushString(valueToken.string);
+ result.SetValue(token.string, values);
+ }
+ else if (result.GetValue(token.string, values))
+ {
+ values.PushString(valueToken.string);
+ }
+ else
+ {
+ result.SetString(token.string, valueToken.string);
+ }
+ }
+ else
+ {
+ PushMappingApiError("ERROR: Entity lump: Unexpected token \"%s\".", valueToken.string);
+ return false;
+ }
+ }
+ case EntlumpTokenType_CloseBrace:
+ {
+ break;
+ }
+ case EntlumpTokenType_EndOfStream:
+ {
+ PushMappingApiError("ERROR: Entity lump: Unexpected end of entity lump! Entity lump parsing failed.");
+ return false;
+ }
+ default:
+ {
+ PushMappingApiError("ERROR: Entity lump: Invalid token \"%s\". Entity lump parsing failed.", token.string);
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+static bool GetHammerIDFromEntityStringMap(int &result, StringMap entity)
+{
+ char hammerID[32];
+ if (!entity.GetString("hammerid", hammerID, sizeof(hammerID))
+ || StringToIntEx(hammerID, result) == 0)
+ {
+ // if we don't have the hammer id, then we can't match the entity to an existing one!
+ char origin[64];
+ entity.GetString("origin", origin, sizeof(origin));
+ PushMappingApiError("ERROR: Failed to parse \"hammerid\" keyvalue on trigger! \"%i\" origin: %s.", result, origin);
+ return false;
+ }
+ return true;
+}
+
+static bool GetAntiBhopTriggerEntityProperties(AntiBhopTrigger result, StringMap entity)
+{
+ if (!GetHammerIDFromEntityStringMap(result.hammerID, entity))
+ {
+ return false;
+ }
+
+ char time[32];
+ if (!entity.GetString("climb_anti_bhop_time", time, sizeof(time))
+ || StringToFloatEx(time, result.time) == 0)
+ {
+ result.time = GOKZ_ANTI_BHOP_TRIGGER_DEFAULT_DELAY;
+ }
+
+ return true;
+}
+
+static bool GetTeleportTriggerEntityProperties(TeleportTrigger result, StringMap entity)
+{
+ if (!GetHammerIDFromEntityStringMap(result.hammerID, entity))
+ {
+ return false;
+ }
+
+ char buffer[64];
+ if (!entity.GetString("climb_teleport_type", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, view_as<int>(result.type)) == 0)
+ {
+ result.type = GOKZ_TELEPORT_TRIGGER_DEFAULT_TYPE;
+ }
+
+ if (!entity.GetString("climb_teleport_destination", result.tpDestination, sizeof(result.tpDestination)))
+ {
+ // We don't want triggers without destinations dangling about, so we need to tell everyone about it!!!
+ PushMappingApiError("ERROR: Could not find \"climb_teleport_destination\" keyvalue on a climb_teleport trigger! hammer id \"%i\".",
+ result.hammerID);
+ return false;
+ }
+
+ if (!entity.GetString("climb_teleport_delay", buffer, sizeof(buffer))
+ || StringToFloatEx(buffer, result.delay) == 0)
+ {
+ result.delay = GOKZ_TELEPORT_TRIGGER_DEFAULT_DELAY;
+ }
+
+ if (!entity.GetString("climb_teleport_use_dest_angles", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.useDestAngles) == 0)
+ {
+ result.useDestAngles = GOKZ_TELEPORT_TRIGGER_DEFAULT_USE_DEST_ANGLES;
+ }
+
+ if (!entity.GetString("climb_teleport_reset_speed", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.resetSpeed) == 0)
+ {
+ result.resetSpeed = GOKZ_TELEPORT_TRIGGER_DEFAULT_RESET_SPEED;
+ }
+
+ if (!entity.GetString("climb_teleport_reorient_player", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.reorientPlayer) == 0)
+ {
+ result.reorientPlayer = GOKZ_TELEPORT_TRIGGER_DEFAULT_REORIENT_PLAYER;
+ }
+
+ if (!entity.GetString("climb_teleport_relative", buffer, sizeof(buffer))
+ || StringToIntEx(buffer, result.relativeDestination) == 0)
+ {
+ result.relativeDestination = GOKZ_TELEPORT_TRIGGER_DEFAULT_RELATIVE_DESTINATION;
+ }
+
+ // NOTE: Clamping
+ if (IsBhopTrigger(result.type))
+ {
+ result.delay = FloatMax(result.delay, GOKZ_TELEPORT_TRIGGER_BHOP_MIN_DELAY);
+ }
+ else
+ {
+ result.delay = FloatMax(result.delay, 0.0);
+ }
+
+ return true;
+}
+
+static void TimerButtonTriggerCheck(char[] touchOutput, int size, StringMap entity, StringMap timerButtonTriggers)
+{
+ int course = 0;
+ char startOutput[128];
+ char endOutput[128];
+ FormatEx(startOutput, sizeof(startOutput), "%s%s%s", GOKZ_START_BUTTON_NAME, CHAR_ESCAPE, "Press");
+ FormatEx(endOutput, sizeof(endOutput), "%s%s%s", GOKZ_END_BUTTON_NAME, CHAR_ESCAPE, "Press");
+ if (StrContains(touchOutput, startOutput, false) != -1)
+ {
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = 0;
+ trigger.isStartTimer = true;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ else if (StrContains(touchOutput, endOutput, false) != -1)
+ {
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = 0;
+ trigger.isStartTimer = false;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ else if (RE_BonusStartButton.Match(touchOutput) > 0)
+ {
+ RE_BonusStartButton.GetSubString(1, touchOutput, sizeof(size));
+ course = StringToInt(touchOutput);
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = course;
+ trigger.isStartTimer = true;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+ else if (RE_BonusEndButton.Match(touchOutput) > 0)
+ {
+ RE_BonusEndButton.GetSubString(1, touchOutput, sizeof(size));
+ course = StringToInt(touchOutput);
+ TimerButtonTrigger trigger;
+ if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity))
+ {
+ trigger.course = course;
+ trigger.isStartTimer = false;
+ }
+ char key[32];
+ IntToString(trigger.hammerID, key, sizeof(key));
+ timerButtonTriggers.SetArray(key, trigger, sizeof(trigger));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/prefix.sp b/sourcemod/scripting/gokz-core/map/prefix.sp
new file mode 100644
index 0000000..3ecbf89
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/prefix.sp
@@ -0,0 +1,48 @@
+/*
+ Mapping API - Prefix
+
+ Detects the map's prefix.
+*/
+
+
+
+static int currentMapPrefix;
+
+
+
+// =====[ PUBLIC ]=====
+
+int GetCurrentMapPrefix()
+{
+ return currentMapPrefix;
+}
+
+
+
+// =====[ LISTENERS ]=====
+
+void OnMapStart_Prefix()
+{
+ char map[PLATFORM_MAX_PATH], mapPrefix[PLATFORM_MAX_PATH];
+ GetCurrentMapDisplayName(map, sizeof(map));
+
+ // Get all characters before the first '_' character
+ for (int i = 0; i < sizeof(mapPrefix); i++)
+ {
+ if (map[i] == '\0' || map[i] == '_')
+ {
+ break;
+ }
+
+ mapPrefix[i] = map[i];
+ }
+
+ if (StrEqual(mapPrefix[0], "kzpro", false))
+ {
+ currentMapPrefix = MapPrefix_KZPro;
+ }
+ else
+ {
+ currentMapPrefix = MapPrefix_Other;
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/starts.sp b/sourcemod/scripting/gokz-core/map/starts.sp
new file mode 100644
index 0000000..94d5b33
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/starts.sp
@@ -0,0 +1,219 @@
+/*
+ Hooks between start destinations and GOKZ.
+*/
+
+
+
+static Regex RE_BonusStart;
+static bool startExists[GOKZ_MAX_COURSES];
+static float startOrigin[GOKZ_MAX_COURSES][3];
+static float startAngles[GOKZ_MAX_COURSES][3];
+
+// Used for SearchStart
+static Regex RE_BonusStartButton;
+static Regex RE_BonusStartZone;
+static CourseTimerType startType[GOKZ_MAX_COURSES];
+static float searchStartOrigin[GOKZ_MAX_COURSES][3];
+static float searchStartAngles[GOKZ_MAX_COURSES][3];
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapStarts()
+{
+ RE_BonusStart = CompileRegex(GOKZ_BONUS_START_NAME_REGEX);
+ RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX);
+ RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX);
+}
+
+void OnEntitySpawned_MapStarts(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+ if (!StrEqual("info_teleport_destination", buffer, false))
+ {
+ return;
+ }
+
+ if (GetEntityName(entity, buffer, sizeof(buffer)) == 0)
+ {
+ return;
+ }
+
+ if (StrEqual(GOKZ_START_NAME, buffer, false))
+ {
+ StoreStart(0, entity);
+ }
+ else
+ {
+ int course = GetStartBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ StoreStart(course, entity);
+ }
+ }
+}
+
+void OnEntitySpawnedPost_MapStarts(int entity)
+{
+ char buffer[32];
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+
+ if (StrEqual("trigger_multiple", buffer, false))
+ {
+ bool isStartZone;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false))
+ {
+ isStartZone = true;
+ StoreSearchStart(0, entity, CourseTimerType_ZoneNew);
+ }
+ else if (GetStartZoneBonusNumber(entity) != -1)
+ {
+ int course = GetStartZoneBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isStartZone = true;
+ StoreSearchStart(course, entity, CourseTimerType_ZoneNew);
+ }
+ }
+ }
+ if (!isStartZone)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer)
+ {
+ StoreSearchStart(trigger.course, entity, CourseTimerType_ZoneLegacy);
+ }
+ }
+
+ }
+ else if (StrEqual("func_button", buffer, false))
+ {
+ bool isStartButton;
+ if (GetEntityName(entity, buffer, sizeof(buffer)) != 0)
+ {
+ if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false))
+ {
+ isStartButton = true;
+ StoreSearchStart(0, entity, CourseTimerType_Button);
+ }
+ else
+ {
+ int course = GetStartButtonBonusNumber(entity);
+ if (GOKZ_IsValidCourse(course, true))
+ {
+ isStartButton = true;
+ StoreSearchStart(course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+ if (!isStartButton)
+ {
+ TimerButtonTrigger trigger;
+ if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer)
+ {
+ StoreSearchStart(trigger.course, entity, CourseTimerType_Button);
+ }
+ }
+ }
+}
+
+void OnMapStart_MapStarts()
+{
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ startExists[course] = false;
+ startType[course] = CourseTimerType_None;
+ }
+}
+
+bool GetMapStartPosition(int course, float origin[3], float angles[3])
+{
+ if (!startExists[course])
+ {
+ return false;
+ }
+
+ origin = startOrigin[course];
+ angles = startAngles[course];
+
+ return true;
+}
+
+bool GetSearchStartPosition(int course, float origin[3], float angles[3])
+{
+ if (startType[course] == CourseTimerType_None)
+ {
+ return false;
+ }
+
+ origin = searchStartOrigin[course];
+ angles = searchStartAngles[course];
+
+ return true;
+}
+
+// =====[ PRIVATE ]=====
+
+static void StoreStart(int course, int entity)
+{
+ float origin[3], angles[3];
+ GetEntPropVector(entity, Prop_Send, "m_vecOrigin", origin);
+ GetEntPropVector(entity, Prop_Data, "m_angRotation", angles);
+ angles[2] = 0.0; // Roll should always be 0.0
+
+ startExists[course] = true;
+ startOrigin[course] = origin;
+ startAngles[course] = angles;
+}
+
+static void StoreSearchStart(int course, int entity, CourseTimerType type)
+{
+ // If StoreSearchStart is called, then there is at least an end position (even though it might not be a valid one)
+ if (startType[course] < CourseTimerType_Default)
+ {
+ startType[course] = CourseTimerType_Default;
+ }
+
+ // Real zone is always better than "fake" zones which are better than buttons
+ // as the buttons found in a map with fake zones aren't meant to be visible.
+ if (startType[course] >= type)
+ {
+ return;
+ }
+
+ float origin[3], distFromCenter[3];
+ GetEntityPositions(entity, origin, searchStartOrigin[course], searchStartAngles[course], distFromCenter);
+
+ // If it is a button or the center of the center of the zone is invalid
+ if (type == CourseTimerType_Button || !IsSpawnValid(searchStartOrigin[course]))
+ {
+ // Attempt with various positions around the entity, pick the first valid one.
+ if (!FindValidPositionAroundTimerEntity(entity, searchStartOrigin[course], searchStartAngles[course], type == CourseTimerType_Button))
+ {
+ searchStartOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone.
+ return;
+ }
+ }
+
+ // Only update the CourseTimerType if a valid position is found.
+ startType[course] = type;
+}
+
+
+static int GetStartBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStart, 1);
+}
+
+static int GetStartButtonBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1);
+}
+
+static int GetStartZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/triggers.sp b/sourcemod/scripting/gokz-core/map/triggers.sp
new file mode 100644
index 0000000..2444493
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/triggers.sp
@@ -0,0 +1,855 @@
+/*
+ Mapping API - Triggers
+
+ Implements trigger related features.
+*/
+
+
+
+static float lastTrigMultiTouchTime[MAXPLAYERS + 1];
+static float lastTrigTeleTouchTime[MAXPLAYERS + 1];
+static float lastTouchGroundOrLadderTime[MAXPLAYERS + 1];
+static int lastTouchSingleBhopEntRef[MAXPLAYERS + 1];
+static ArrayList lastTouchSequentialBhopEntRefs[MAXPLAYERS + 1];
+static int triggerTouchCount[MAXPLAYERS + 1];
+static int antiCpTriggerTouchCount[MAXPLAYERS + 1];
+static int antiPauseTriggerTouchCount[MAXPLAYERS + 1];
+static int antiJumpstatTriggerTouchCount[MAXPLAYERS + 1];
+static int mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+static int bhopTouchCount[MAXPLAYERS + 1];
+static bool jumpedThisTick[MAXPLAYERS + 1];
+static float jumpOrigin[MAXPLAYERS + 1][3];
+static float jumpVelocity[MAXPLAYERS + 1][3];
+static ArrayList triggerTouchList[MAXPLAYERS + 1]; // arraylist of TouchedTrigger that the player is currently touching. this array won't ever get long (unless the mapper does something weird).
+static StringMap triggerTouchCounts[MAXPLAYERS + 1]; // stringmap of int touch counts with key being a string of the entity reference.
+static StringMap antiBhopTriggers; // stringmap of AntiBhopTrigger with key being a string of the m_iHammerID entprop.
+static StringMap teleportTriggers; // stringmap of TeleportTrigger with key being a string of the m_iHammerID entprop.
+static StringMap timerButtonTriggers; // stringmap of legacy timer zone triggers with key being a string of the m_iHammerID entprop.
+static ArrayList parseErrorStrings;
+
+
+
+// =====[ PUBLIC ]=====
+
+bool BhopTriggersJustTouched(int client)
+{
+ // NOTE: This is slightly incorrect since we touch triggers in the air, but
+ // it doesn't matter since we can't checkpoint in the air.
+ if (bhopTouchCount[client] > 0)
+ {
+ return true;
+ }
+ // GetEngineTime return changes between calls. We only call it once at the beginning.
+ float engineTime = GetEngineTime();
+ // If the player touches a teleport trigger, increase the delay required
+ if (engineTime - lastTouchGroundOrLadderTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched ground or ladder
+ && engineTime - lastTrigMultiTouchTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched trigger_multiple
+ || engineTime - lastTrigTeleTouchTime[client] < GOKZ_BHOP_NO_CHECKPOINT_TIME) // Just touched trigger_teleport
+ {
+ return true;
+ }
+
+ return Movement_GetMovetype(client) == MOVETYPE_LADDER
+ && triggerTouchCount[client] > 0
+ && engineTime - lastTrigTeleTouchTime[client] < GOKZ_LADDER_NO_CHECKPOINT_TIME;
+}
+
+bool AntiCpTriggerIsTouched(int client)
+{
+ return antiCpTriggerTouchCount[client] > 0;
+}
+
+bool AntiPauseTriggerIsTouched(int client)
+{
+ return antiPauseTriggerTouchCount[client] > 0;
+}
+
+void PushMappingApiError(char[] format, any ...)
+{
+ char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH];
+ VFormat(error, sizeof(error), format, 2);
+ parseErrorStrings.PushString(error);
+}
+
+TriggerType GetTriggerType(char[] targetName)
+{
+ TriggerType result = TriggerType_Invalid;
+
+ if (StrEqual(targetName, GOKZ_ANTI_BHOP_TRIGGER_NAME))
+ {
+ result = TriggerType_Antibhop;
+ }
+ else if (StrEqual(targetName, GOKZ_TELEPORT_TRIGGER_NAME))
+ {
+ result = TriggerType_Teleport;
+ }
+
+ return result;
+}
+
+bool IsBhopTrigger(TeleportType type)
+{
+ return type == TeleportType_MultiBhop
+ || type == TeleportType_SingleBhop
+ || type == TeleportType_SequentialBhop;
+}
+
+bool IsTimerButtonTrigger(int entity, TimerButtonTrigger trigger)
+{
+ char hammerID[32];
+ bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID));
+ if (gotHammerID && timerButtonTriggers.GetArray(hammerID, trigger, sizeof(trigger)))
+ {
+ return true;
+ }
+ return false;
+}
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapTriggers()
+{
+ parseErrorStrings = new ArrayList(ByteCountToCells(GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH));
+ antiBhopTriggers = new StringMap();
+ teleportTriggers = new StringMap();
+ timerButtonTriggers = new StringMap();
+}
+
+void OnMapStart_MapTriggers()
+{
+ parseErrorStrings.Clear();
+ antiBhopTriggers.Clear();
+ teleportTriggers.Clear();
+ timerButtonTriggers.Clear();
+ mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE;
+ EntlumpParse(antiBhopTriggers, teleportTriggers, timerButtonTriggers, mapMappingApiVersion);
+
+ if (mapMappingApiVersion > GOKZ_MAPPING_API_VERSION)
+ {
+ SetFailState("Map's mapping api version is too big! Maximum supported version is %i, but map has %i. If you're not on the latest GOKZ version, then update!",
+ GOKZ_MAPPING_API_VERSION, mapMappingApiVersion);
+ }
+}
+
+void OnClientPutInServer_MapTriggers(int client)
+{
+ triggerTouchCount[client] = 0;
+ antiCpTriggerTouchCount[client] = 0;
+ antiPauseTriggerTouchCount[client] = 0;
+ antiJumpstatTriggerTouchCount[client] = 0;
+
+ if (triggerTouchList[client] == null)
+ {
+ triggerTouchList[client] = new ArrayList(sizeof(TouchedTrigger));
+ }
+ else
+ {
+ triggerTouchList[client].Clear();
+ }
+
+ if (triggerTouchCounts[client] == null)
+ {
+ triggerTouchCounts[client] = new StringMap();
+ }
+ else
+ {
+ triggerTouchCounts[client].Clear();
+ }
+
+ bhopTouchCount[client] = 0;
+
+ if (lastTouchSequentialBhopEntRefs[client] == null)
+ {
+ lastTouchSequentialBhopEntRefs[client] = new ArrayList();
+ }
+ else
+ {
+ lastTouchSequentialBhopEntRefs[client].Clear();
+ }
+}
+
+void OnPlayerRunCmd_MapTriggers(int client, int &buttons)
+{
+ int flags = GetEntityFlags(client);
+ MoveType moveType = GetEntityMoveType(client);
+
+ // if the player isn't touching any bhop triggers on ground/a ladder, then
+ // reset the singlebhop and sequential bhop state.
+ if ((flags & FL_ONGROUND || moveType == MOVETYPE_LADDER)
+ && bhopTouchCount[client] == 0)
+ {
+ ResetBhopState(client);
+ }
+
+ if (antiJumpstatTriggerTouchCount[client] > 0)
+ {
+ if (GetFeatureStatus(FeatureType_Native, "GOKZ_JS_InvalidateJump") == FeatureStatus_Available)
+ {
+ GOKZ_JS_InvalidateJump(client);
+ }
+ }
+
+ // Check if we're touching any triggers and act accordingly.
+ // NOTE: Read through the touch list in reverse order, so some
+ // trigger behaviours will be better. Trust me!
+ int triggerTouchListLength = triggerTouchList[client].Length;
+ for (int i = triggerTouchListLength - 1; i >= 0; i--)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+
+ if (touched.triggerType == TriggerType_Antibhop)
+ {
+ TouchAntibhopTrigger(client, touched, buttons, flags);
+ }
+ else if (touched.triggerType == TriggerType_Teleport)
+ {
+ // Sometimes due to lag or whatever, the player can be
+ // teleported twice by the same trigger. This fixes that.
+ if (TouchTeleportTrigger(client, touched, flags))
+ {
+ RemoveTriggerFromTouchList(client, EntRefToEntIndex(touched.entRef));
+ i--;
+ triggerTouchListLength--;
+ }
+ }
+ }
+ jumpedThisTick[client] = false;
+}
+
+void OnPlayerSpawn_MapTriggers(int client)
+{
+ // Print trigger errors every time a player spawns so that
+ // mappers and testers can very easily spot mistakes in names
+ // and get them fixed asap.
+ if (parseErrorStrings.Length > 0)
+ {
+ char errStart[] = "ERROR: Errors detected when trying to load triggers!";
+ CPrintToChat(client, "{red}%s", errStart);
+ PrintToConsole(client, "\n%s", errStart);
+
+ int length = parseErrorStrings.Length;
+ for (int err = 0; err < length; err++)
+ {
+ char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH];
+ parseErrorStrings.GetString(err, error, sizeof(error));
+ CPrintToChat(client, "{red}%s", error);
+ PrintToConsole(client, error);
+ }
+ CPrintToChat(client, "{red}If the errors get clipped off in the chat, then look in your developer console!\n");
+ }
+}
+
+public void OnPlayerJump_Triggers(int client)
+{
+ jumpedThisTick[client] = true;
+ GetClientAbsOrigin(client, jumpOrigin[client]);
+ Movement_GetVelocity(client, jumpVelocity[client]);
+}
+
+void OnEntitySpawned_MapTriggers(int entity)
+{
+ char classname[32];
+ GetEntityClassname(entity, classname, sizeof(classname));
+ char name[64];
+ GetEntityName(entity, name, sizeof(name));
+
+ bool triggerMultiple = StrEqual("trigger_multiple", classname);
+ if (triggerMultiple)
+ {
+ char hammerID[32];
+ bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID));
+
+ if (StrEqual(GOKZ_TELEPORT_TRIGGER_NAME, name))
+ {
+ TeleportTrigger teleportTrigger;
+ if (gotHammerID && teleportTriggers.GetArray(hammerID, teleportTrigger, sizeof(teleportTrigger)))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnTeleportTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnTeleportTrigTouchEnd_MapTriggers);
+ }
+ else
+ {
+ PushMappingApiError("ERROR: Couldn't match teleport trigger's Hammer ID %s with any Hammer ID from the map.", hammerID);
+ }
+ }
+ else if (StrEqual(GOKZ_ANTI_BHOP_TRIGGER_NAME, name))
+ {
+ AntiBhopTrigger antiBhopTrigger;
+ if (gotHammerID && antiBhopTriggers.GetArray(hammerID, antiBhopTrigger, sizeof(antiBhopTrigger)))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiBhopTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiBhopTrigTouchEnd_MapTriggers);
+ }
+ else
+ {
+ PushMappingApiError("ERROR: Couldn't match antibhop trigger's Hammer ID %s with any Hammer ID from the map.", hammerID);
+ }
+ }
+ else if (StrEqual(GOKZ_BHOP_RESET_TRIGGER_NAME, name))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnBhopResetTouchStart_MapTriggers);
+ }
+ else if (StrEqual(GOKZ_ANTI_CP_TRIGGER_NAME, name, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiCpTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiCpTrigTouchEnd_MapTriggers);
+ }
+ else if (StrEqual(GOKZ_ANTI_PAUSE_TRIGGER_NAME, name, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiPauseTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiPauseTrigTouchEnd_MapTriggers);
+ }
+ else if (StrEqual(GOKZ_ANTI_JUMPSTAT_TRIGGER_NAME, name, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnAntiJumpstatTrigTouchStart_MapTriggers);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnAntiJumpstatTrigTouchEnd_MapTriggers);
+ }
+ else
+ {
+ // NOTE: SDKHook touch hooks bypass trigger filters. We want that only with
+ // non mapping api triggers because it prevents checkpointing on bhop blocks.
+ SDKHook(entity, SDKHook_StartTouchPost, OnTrigMultTouchStart_MapTriggers);
+ SDKHook(entity, SDKHook_EndTouchPost, OnTrigMultTouchEnd_MapTriggers);
+ }
+ }
+ else if (StrEqual("trigger_teleport", classname))
+ {
+ SDKHook(entity, SDKHook_StartTouchPost, OnTrigTeleTouchStart_MapTriggers);
+ SDKHook(entity, SDKHook_EndTouchPost, OnTrigTeleTouchEnd_MapTriggers);
+ }
+}
+
+public void OnAntiBhopTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ int touchCount = IncrementTriggerTouchCount(other, entity);
+ if (touchCount <= 0)
+ {
+ // The trigger has fired a matching endtouch output before
+ // the starttouch output, so ignore it.
+ return;
+ }
+
+ if (jumpedThisTick[other])
+ {
+ TeleportEntity(other, jumpOrigin[other], NULL_VECTOR, jumpVelocity[other]);
+ }
+
+ AddTriggerToTouchList(other, entity, TriggerType_Antibhop);
+}
+
+public void OnAntiBhopTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ DecrementTriggerTouchCount(other, entity);
+ RemoveTriggerFromTouchList(other, entity);
+}
+
+public void OnTeleportTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ int touchCount = IncrementTriggerTouchCount(other, entity);
+ if (touchCount <= 0)
+ {
+ // The trigger has fired a matching endtouch output before
+ // the starttouch output, so ignore it.
+ return;
+ }
+
+ char key[32];
+ GetEntityHammerIDString(entity, key, sizeof(key));
+ TeleportTrigger trigger;
+ if (teleportTriggers.GetArray(key, trigger, sizeof(trigger))
+ && IsBhopTrigger(trigger.type))
+ {
+ bhopTouchCount[other]++;
+ }
+
+ AddTriggerToTouchList(other, entity, TriggerType_Teleport);
+}
+
+public void OnTeleportTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ DecrementTriggerTouchCount(other, entity);
+
+ char key[32];
+ GetEntityHammerIDString(entity, key, sizeof(key));
+ TeleportTrigger trigger;
+ if (teleportTriggers.GetArray(key, trigger, sizeof(trigger))
+ && IsBhopTrigger(trigger.type))
+ {
+ bhopTouchCount[other]--;
+ }
+
+ RemoveTriggerFromTouchList(other, entity);
+}
+
+public void OnBhopResetTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ ResetBhopState(other);
+}
+
+public void OnAntiCpTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiCpTriggerTouchCount[other]++;
+}
+
+public void OnAntiCpTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiCpTriggerTouchCount[other]--;
+}
+
+public void OnAntiPauseTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiPauseTriggerTouchCount[other]++;
+}
+
+public void OnAntiPauseTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiPauseTriggerTouchCount[other]--;
+}
+
+public void OnAntiJumpstatTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiJumpstatTriggerTouchCount[other]++;
+}
+
+public void OnAntiJumpstatTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ antiJumpstatTriggerTouchCount[other]--;
+}
+
+public void OnTrigMultTouchStart_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ lastTrigMultiTouchTime[other] = GetEngineTime();
+ triggerTouchCount[other]++;
+}
+
+public void OnTrigMultTouchEnd_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ triggerTouchCount[other]--;
+}
+
+public void OnTrigTeleTouchStart_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ lastTrigTeleTouchTime[other] = GetEngineTime();
+ triggerTouchCount[other]++;
+}
+
+public void OnTrigTeleTouchEnd_MapTriggers(int entity, int other)
+{
+ if (!IsValidClient(other))
+ {
+ return;
+ }
+
+ triggerTouchCount[other]--;
+}
+
+void OnStartTouchGround_MapTriggers(int client)
+{
+ lastTouchGroundOrLadderTime[client] = GetEngineTime();
+
+ for (int i = 0; i < triggerTouchList[client].Length; i++)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+ // set the touched tick to the tick that the player touches the ground.
+ touched.groundTouchTick = gI_TickCount[client];
+ triggerTouchList[client].SetArray(i, touched);
+ }
+}
+
+void OnStopTouchGround_MapTriggers(int client)
+{
+ for (int i = 0; i < triggerTouchList[client].Length; i++)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+
+ if (touched.triggerType == TriggerType_Teleport)
+ {
+ char key[32];
+ GetEntityHammerIDString(touched.entRef, key, sizeof(key));
+ TeleportTrigger trigger;
+ // set last touched triggers for single and sequential bhop.
+ if (teleportTriggers.GetArray(key, trigger, sizeof(trigger))
+ && IsBhopTrigger(trigger.type))
+ {
+ if (trigger.type == TeleportType_SequentialBhop)
+ {
+ lastTouchSequentialBhopEntRefs[client].Push(touched.entRef);
+ }
+ // NOTE: For singlebhops, we don't care which type of bhop we last touched, because
+ // otherwise jumping back and forth between a multibhop and a singlebhop wouldn't work.
+ if (i == 0 && IsBhopTrigger(trigger.type))
+ {
+ // We only want to set this once in this loop.
+ lastTouchSingleBhopEntRef[client] = touched.entRef;
+ }
+ }
+ }
+ }
+}
+
+void OnChangeMovetype_MapTriggers(int client, MoveType newMovetype)
+{
+ if (newMovetype == MOVETYPE_LADDER)
+ {
+ lastTouchGroundOrLadderTime[client] = GetEngineTime();
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void AddTriggerToTouchList(int client, int trigger, TriggerType triggerType)
+{
+ int triggerEntRef = EntIndexToEntRef(trigger);
+
+ TouchedTrigger touched;
+ touched.triggerType = triggerType;
+ touched.entRef = triggerEntRef;
+ touched.startTouchTick = gI_TickCount[client];
+ touched.groundTouchTick = -1;
+ if (GetEntityFlags(client) & FL_ONGROUND)
+ {
+ touched.groundTouchTick = gI_TickCount[client];
+ }
+
+ triggerTouchList[client].PushArray(touched);
+}
+
+static void RemoveTriggerFromTouchList(int client, int trigger)
+{
+ int triggerEntRef = EntIndexToEntRef(trigger);
+ for (int i = 0; i < triggerTouchList[client].Length; i++)
+ {
+ TouchedTrigger touched;
+ triggerTouchList[client].GetArray(i, touched);
+ if (touched.entRef == triggerEntRef)
+ {
+ triggerTouchList[client].Erase(i);
+ break;
+ }
+ }
+}
+
+static int IncrementTriggerTouchCount(int client, int trigger)
+{
+ int entref = EntIndexToEntRef(trigger);
+ char szEntref[64];
+ FormatEx(szEntref, sizeof(szEntref), "%i", entref);
+
+ int value = 0;
+ triggerTouchCounts[client].GetValue(szEntref, value);
+
+ value += 1;
+ triggerTouchCounts[client].SetValue(szEntref, value);
+
+ return value;
+}
+
+static void DecrementTriggerTouchCount(int client, int trigger)
+{
+ int entref = EntIndexToEntRef(trigger);
+ char szEntref[64];
+ FormatEx(szEntref, sizeof(szEntref), "%i", entref);
+
+ int value = 0;
+ triggerTouchCounts[client].GetValue(szEntref, value);
+
+ value -= 1;
+ triggerTouchCounts[client].SetValue(szEntref, value);
+}
+
+static void TouchAntibhopTrigger(int client, TouchedTrigger touched, int &newButtons, int flags)
+{
+ if (!(flags & FL_ONGROUND))
+ {
+ // Disable jump when the player is in the air.
+ // This is a very simple way to fix jumpbugging antibhop triggers.
+ newButtons &= ~IN_JUMP;
+ return;
+ }
+
+ if (touched.groundTouchTick == -1)
+ {
+ // The player hasn't touched the ground inside this trigger yet.
+ return;
+ }
+
+ char key[32];
+ GetEntityHammerIDString(touched.entRef, key, sizeof(key));
+ AntiBhopTrigger trigger;
+ if (antiBhopTriggers.GetArray(key, trigger, sizeof(trigger)))
+ {
+ float touchTime = CalculateGroundTouchTime(client, touched);
+ if (trigger.time == 0.0 || touchTime <= trigger.time)
+ {
+ // disable jump
+ newButtons &= ~IN_JUMP;
+ }
+ }
+}
+
+static bool TouchTeleportTrigger(int client, TouchedTrigger touched, int flags)
+{
+ bool shouldTeleport = false;
+
+ char key[32];
+ GetEntityHammerIDString(touched.entRef, key, sizeof(key));
+ TeleportTrigger trigger;
+ if (!teleportTriggers.GetArray(key, trigger, sizeof(trigger)))
+ {
+ // Couldn't get the teleport trigger from the trigger array for some reason.
+ return shouldTeleport;
+ }
+
+ bool isBhopTrigger = IsBhopTrigger(trigger.type);
+ // NOTE: Player hasn't touched the ground inside this trigger yet.
+ if (touched.groundTouchTick == -1 && isBhopTrigger)
+ {
+ return shouldTeleport;
+ }
+
+ float destOrigin[3];
+ float destAngles[3];
+ bool gotDestOrigin;
+ bool gotDestAngles;
+ int destinationEnt = GetTeleportDestinationAndOrientation(trigger.tpDestination, destOrigin, destAngles, gotDestOrigin, gotDestAngles);
+
+ float triggerOrigin[3];
+ bool gotTriggerOrigin = GetEntityAbsOrigin(touched.entRef, triggerOrigin);
+
+ // NOTE: We only use the trigger's origin if we're using a relative destination, so if
+ // we're not using a relative destination and don't have it, then it's fine.
+ if (!IsValidEntity(destinationEnt) || !gotDestOrigin
+ || (!gotTriggerOrigin && trigger.relativeDestination))
+ {
+ PrintToConsole(client, "[KZ] Invalid teleport destination \"%s\" on trigger with hammerID %i.", trigger.tpDestination, trigger.hammerID);
+ return shouldTeleport;
+ }
+
+ // NOTE: Find out if we should actually teleport.
+ if (isBhopTrigger && (flags & FL_ONGROUND))
+ {
+ float touchTime = CalculateGroundTouchTime(client, touched);
+ if (touchTime > trigger.delay)
+ {
+ shouldTeleport = true;
+ }
+ else if (trigger.type == TeleportType_SingleBhop)
+ {
+ shouldTeleport = lastTouchSingleBhopEntRef[client] == touched.entRef;
+ }
+ else if (trigger.type == TeleportType_SequentialBhop)
+ {
+ int length = lastTouchSequentialBhopEntRefs[client].Length;
+ for (int j = 0; j < length; j++)
+ {
+ int entRef = lastTouchSequentialBhopEntRefs[client].Get(j);
+ if (entRef == touched.entRef)
+ {
+ shouldTeleport = true;
+ break;
+ }
+ }
+ }
+ }
+ else if (trigger.type == TeleportType_Normal)
+ {
+ float touchTime = CalculateStartTouchTime(client, touched);
+ shouldTeleport = touchTime > trigger.delay || (trigger.delay == 0.0);
+ }
+
+ if (!shouldTeleport)
+ {
+ return shouldTeleport;
+ }
+
+ bool shouldReorientPlayer = trigger.reorientPlayer
+ && gotDestAngles && (destAngles[1] != 0.0);
+
+ float zAxis[3];
+ zAxis = view_as<float>({0.0, 0.0, 1.0});
+
+ // NOTE: Work out finalOrigin.
+ float finalOrigin[3];
+ if (trigger.relativeDestination)
+ {
+ float playerOrigin[3];
+ Movement_GetOrigin(client, playerOrigin);
+
+ float playerOffsetFromTrigger[3];
+ SubtractVectors(playerOrigin, triggerOrigin, playerOffsetFromTrigger);
+
+ if (shouldReorientPlayer)
+ {
+ // NOTE: rotate player offset by the destination trigger's yaw.
+ RotateVectorAxis(playerOffsetFromTrigger, zAxis, DegToRad(destAngles[1]), playerOffsetFromTrigger);
+ }
+
+ AddVectors(destOrigin, playerOffsetFromTrigger, finalOrigin);
+ }
+ else
+ {
+ finalOrigin = destOrigin;
+ }
+
+ // NOTE: Work out finalPlayerAngles.
+ float finalPlayerAngles[3];
+ Movement_GetEyeAngles(client, finalPlayerAngles);
+ if (shouldReorientPlayer)
+ {
+ finalPlayerAngles[1] -= destAngles[1];
+
+ float velocity[3];
+ Movement_GetVelocity(client, velocity);
+
+ // NOTE: rotate velocity by the destination trigger's yaw.
+ RotateVectorAxis(velocity, zAxis, DegToRad(destAngles[1]), velocity);
+ Movement_SetVelocity(client, velocity);
+ }
+ else if (!trigger.reorientPlayer && trigger.useDestAngles)
+ {
+ finalPlayerAngles = destAngles;
+ }
+
+ if (shouldTeleport)
+ {
+ TeleportPlayer(client, finalOrigin, finalPlayerAngles, gotDestAngles && trigger.useDestAngles, trigger.resetSpeed);
+ }
+
+ return shouldTeleport;
+}
+
+static float CalculateGroundTouchTime(int client, TouchedTrigger touched)
+{
+ float result = float(gI_TickCount[client] - touched.groundTouchTick) * GetTickInterval();
+ return result;
+}
+
+static float CalculateStartTouchTime(int client, TouchedTrigger touched)
+{
+ float result = float(gI_TickCount[client] - touched.startTouchTick) * GetTickInterval();
+ return result;
+}
+
+static void ResetBhopState(int client)
+{
+ lastTouchSingleBhopEntRef[client] = INVALID_ENT_REFERENCE;
+ lastTouchSequentialBhopEntRefs[client].Clear();
+}
+
+static bool GetEntityHammerIDString(int entity, char[] buffer, int maxLength)
+{
+ if (!IsValidEntity(entity))
+ {
+ return false;
+ }
+
+ if (!HasEntProp(entity, Prop_Data, "m_iHammerID"))
+ {
+ return false;
+ }
+
+ int hammerID = GetEntProp(entity, Prop_Data, "m_iHammerID");
+ IntToString(hammerID, buffer, maxLength);
+
+ return true;
+}
+
+// NOTE: returns an entity reference (possibly invalid).
+static int GetTeleportDestinationAndOrientation(char[] targetName, float origin[3], float angles[3] = NULL_VECTOR, bool &gotOrigin = false, bool &gotAngles = false)
+{
+ // NOTE: We're not caching the teleport destination because it could change.
+ int destination = GOKZFindEntityByName(targetName, .ignorePlayers = true);
+ if (!IsValidEntity(destination))
+ {
+ return destination;
+ }
+
+ gotOrigin = GetEntityAbsOrigin(destination, origin);
+
+ if (HasEntProp(destination, Prop_Data, "m_angAbsRotation"))
+ {
+ GetEntPropVector(destination, Prop_Data, "m_angAbsRotation", angles);
+ gotAngles = true;
+ }
+ else
+ {
+ gotAngles = false;
+ }
+
+ return destination;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/map/zones.sp b/sourcemod/scripting/gokz-core/map/zones.sp
new file mode 100644
index 0000000..684e42d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/map/zones.sp
@@ -0,0 +1,183 @@
+/*
+ Hooks between specifically named trigger_multiples and GOKZ.
+*/
+
+
+
+static Regex RE_BonusStartZone;
+static Regex RE_BonusEndZone;
+static bool touchedGroundSinceTouchingStartZone[MAXPLAYERS + 1];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_MapZones()
+{
+ RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX);
+ RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX);
+}
+
+void OnStartTouchGround_MapZones(int client)
+{
+ touchedGroundSinceTouchingStartZone[client] = true;
+}
+
+void OnEntitySpawned_MapZones(int entity)
+{
+ char buffer[32];
+
+ GetEntityClassname(entity, buffer, sizeof(buffer));
+ if (!StrEqual("trigger_multiple", buffer, false))
+ {
+ return;
+ }
+
+ if (GetEntityName(entity, buffer, sizeof(buffer)) == 0)
+ {
+ return;
+ }
+
+ int course = 0;
+ if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnStartZoneStartTouch);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnStartZoneEndTouch);
+ RegisterCourseStart(course);
+ }
+ else if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false))
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnEndZoneStartTouch);
+ RegisterCourseEnd(course);
+ }
+ else if ((course = GetStartZoneBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnBonusStartZoneStartTouch);
+ HookSingleEntityOutput(entity, "OnEndTouch", OnBonusStartZoneEndTouch);
+ RegisterCourseStart(course);
+ }
+ else if ((course = GetEndZoneBonusNumber(entity)) != -1)
+ {
+ HookSingleEntityOutput(entity, "OnStartTouch", OnBonusEndZoneStartTouch);
+ RegisterCourseEnd(course);
+ }
+}
+
+public void OnStartZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessStartZoneStartTouch(activator, 0);
+}
+
+public void OnStartZoneEndTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessStartZoneEndTouch(activator, 0);
+}
+
+public void OnEndZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ ProcessEndZoneStartTouch(activator, 0);
+}
+
+public void OnBonusStartZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetStartZoneBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessStartZoneStartTouch(activator, course);
+}
+
+public void OnBonusStartZoneEndTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetStartZoneBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessStartZoneEndTouch(activator, course);
+}
+
+public void OnBonusEndZoneStartTouch(const char[] name, int caller, int activator, float delay)
+{
+ if (!IsValidEntity(caller) || !IsValidClient(activator))
+ {
+ return;
+ }
+
+ int course = GetEndZoneBonusNumber(caller);
+ if (!GOKZ_IsValidCourse(course, true))
+ {
+ return;
+ }
+
+ ProcessEndZoneStartTouch(activator, course);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void ProcessStartZoneStartTouch(int client, int course)
+{
+ touchedGroundSinceTouchingStartZone[client] = Movement_GetOnGround(client);
+
+ GOKZ_StopTimer(client, false);
+ SetCurrentCourse(client, course);
+
+ OnStartZoneStartTouch_Teleports(client, course);
+}
+
+static void ProcessStartZoneEndTouch(int client, int course)
+{
+ if (!touchedGroundSinceTouchingStartZone[client])
+ {
+ return;
+ }
+
+ GOKZ_StartTimer(client, course, true);
+ GOKZ_ResetVirtualButtonPosition(client, true);
+}
+
+static void ProcessEndZoneStartTouch(int client, int course)
+{
+ GOKZ_EndTimer(client, course);
+ GOKZ_ResetVirtualButtonPosition(client, false);
+}
+
+static int GetStartZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1);
+}
+
+static int GetEndZoneBonusNumber(int entity)
+{
+ return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/menus/mode_menu.sp b/sourcemod/scripting/gokz-core/menus/mode_menu.sp
new file mode 100644
index 0000000..934d29c
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/menus/mode_menu.sp
@@ -0,0 +1,40 @@
+/*
+ Lets players choose their mode.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_Mode);
+ menu.SetTitle("%T", "Mode Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, true);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public int MenuHandler_Mode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ GOKZ_SetCoreOption(param1, Option_Mode, param2);
+ if (GetCameFromOptionsMenu(param1))
+ {
+ DisplayOptionsMenu(param1, TopMenuPosition_LastCategory);
+ }
+ }
+ else if (action == MenuAction_Cancel && GetCameFromOptionsMenu(param1))
+ {
+ DisplayOptionsMenu(param1, TopMenuPosition_LastCategory);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/menus/options_menu.sp b/sourcemod/scripting/gokz-core/menus/options_menu.sp
new file mode 100644
index 0000000..240ee81
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/menus/options_menu.sp
@@ -0,0 +1,174 @@
+/*
+ TopMenu that allows users to browse categories of options.
+
+ Adds core options to the general category where players
+ can cycle the value of each core option.
+*/
+
+
+
+static TopMenu optionsMenu;
+static TopMenuObject catGeneral;
+static TopMenuObject itemsGeneral[OPTION_COUNT];
+static bool cameFromOptionsMenu[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayOptionsMenu(int client, TopMenuPosition position = TopMenuPosition_Start)
+{
+ optionsMenu.Display(client, position);
+ cameFromOptionsMenu[client] = false;
+}
+
+TopMenu GetOptionsTopMenu()
+{
+ return optionsMenu;
+}
+
+bool GetCameFromOptionsMenu(int client)
+{
+ return cameFromOptionsMenu[client];
+}
+
+
+
+// =====[ LISTENERS ]=====
+
+void OnAllPluginsLoaded_OptionsMenu()
+{
+ optionsMenu = new TopMenu(TopMenuHandler_Options);
+ Call_GOKZ_OnOptionsMenuCreated(optionsMenu);
+ Call_GOKZ_OnOptionsMenuReady(optionsMenu);
+}
+
+void OnConfigsExecuted_OptionsMenu()
+{
+ SortOptionsMenu();
+}
+
+void OnOptionsMenuCreated_OptionsMenu()
+{
+ catGeneral = optionsMenu.AddCategory(GENERAL_OPTION_CATEGORY, TopMenuHandler_Options);
+}
+
+void OnOptionsMenuReady_OptionsMenu()
+{
+ for (int option = 0; option < view_as<int>(OPTION_COUNT); option++)
+ {
+ if (option == view_as<int>(Option_Style))
+ {
+ continue; // TODO Currently hard-coded to skip style
+ }
+ itemsGeneral[option] = optionsMenu.AddItem(gC_CoreOptionNames[option], TopMenuHandler_General, catGeneral);
+ }
+}
+
+
+
+// =====[ HANDLER ]=====
+
+public void TopMenuHandler_Options(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == INVALID_TOPMENUOBJECT)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - Title", param);
+ }
+ else if (topobj_id == catGeneral)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - General", param);
+ }
+ }
+}
+
+public void TopMenuHandler_General(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ Option option = OPTION_INVALID;
+ for (int i = 0; i < view_as<int>(OPTION_COUNT); i++)
+ {
+ if (topobj_id == itemsGeneral[i])
+ {
+ option = view_as<Option>(i);
+ break;
+ }
+ }
+
+ if (option == OPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case Option_Mode:
+ {
+ FormatEx(buffer, maxlength, "%T - %s",
+ gC_CoreOptionPhrases[option], param,
+ gC_ModeNames[GOKZ_GetCoreOption(param, option)]);
+ }
+ case Option_TimerButtonZoneType:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], param,
+ gC_TimerButtonZoneTypePhrases[GOKZ_GetCoreOption(param, option)], param);
+ }
+ case Option_Safeguard:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], param,
+ gC_SafeGuardPhrases[GOKZ_GetCoreOption(param, option)], param);
+ }
+ default:FormatToggleableOptionDisplay(param, option, buffer, maxlength);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ switch (option)
+ {
+ case Option_Mode:
+ {
+ cameFromOptionsMenu[param] = true;
+ DisplayModeMenu(param);
+ }
+ default:
+ {
+ GOKZ_CycleCoreOption(param, option);
+ optionsMenu.Display(param, TopMenuPosition_LastCategory);
+ }
+ }
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void SortOptionsMenu()
+{
+ char error[256];
+ if (!optionsMenu.LoadConfig(GOKZ_CFG_OPTIONS_SORTING, error, sizeof(error)))
+ {
+ LogError("Failed to load file: \"%s\". Error: %s", GOKZ_CFG_OPTIONS_SORTING, error);
+ }
+}
+
+static void FormatToggleableOptionDisplay(int client, Option option, char[] buffer, int maxlength)
+{
+ if (GOKZ_GetCoreOption(client, option) == 0)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_CoreOptionPhrases[option], client,
+ "Options Menu - Enabled", client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/misc.sp b/sourcemod/scripting/gokz-core/misc.sp
new file mode 100644
index 0000000..a117880
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/misc.sp
@@ -0,0 +1,803 @@
+/*
+ Small features that aren't worth splitting into their own file.
+*/
+
+
+
+// =====[ GOKZ.CFG ]=====
+
+void OnMapStart_KZConfig()
+{
+ char gokzCfgFullPath[PLATFORM_MAX_PATH];
+ FormatEx(gokzCfgFullPath, sizeof(gokzCfgFullPath), "cfg/%s", GOKZ_CFG_SERVER);
+
+ if (FileExists(gokzCfgFullPath))
+ {
+ ServerCommand("exec %s", GOKZ_CFG_SERVER);
+ }
+ else
+ {
+ SetFailState("Failed to load file: \"%s\". Check that it exists.", gokzCfgFullPath);
+ }
+}
+
+
+
+// =====[ GODMODE ]=====
+
+void OnPlayerSpawn_GodMode(int client)
+{
+ // Stop players from taking damage
+ SetEntProp(client, Prop_Data, "m_takedamage", 0);
+ SetEntityFlags(client, GetEntityFlags(client) | FL_GODMODE);
+}
+
+
+
+// =====[ NOCLIP ]=====
+
+int noclipReleaseTime[MAXPLAYERS + 1];
+
+void ToggleNoclip(int client)
+{
+ if (Movement_GetMovetype(client) != MOVETYPE_NOCLIP)
+ {
+ EnableNoclip(client);
+ }
+ else
+ {
+ DisableNoclip(client);
+ }
+}
+
+void EnableNoclip(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client))
+ {
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+ Movement_SetMovetype(client, MOVETYPE_NOCLIP);
+ GOKZ_StopTimer(client);
+ }
+}
+
+void DisableNoclip(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP)
+ {
+ noclipReleaseTime[client] = GetGameTickCount();
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Prevents an exploit that would let you noclip out of start zones
+ RemoveNoclipGroundFlag(client);
+ }
+}
+
+void ToggleNoclipNotrigger(int client)
+{
+ if (Movement_GetMovetype(client) != MOVETYPE_NOCLIP)
+ {
+ EnableNoclipNotrigger(client);
+ }
+ else
+ {
+ DisableNoclipNotrigger(client);
+ }
+}
+
+void EnableNoclipNotrigger(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client))
+ {
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+ Movement_SetMovetype(client, MOVETYPE_NOCLIP);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER);
+ GOKZ_StopTimer(client);
+ }
+}
+
+void DisableNoclipNotrigger(int client)
+{
+ if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP)
+ {
+ noclipReleaseTime[client] = GetGameTickCount();
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Prevents an exploit that would let you noclip out of start zones
+ RemoveNoclipGroundFlag(client);
+ }
+}
+
+void RemoveNoclipGroundFlag(int client)
+{
+ float startPosition[3], endPosition[3];
+ GetClientAbsOrigin(client, startPosition);
+ endPosition = startPosition;
+ endPosition[2] = startPosition[2] - 2.0;
+ Handle trace = TR_TraceHullFilterEx(
+ startPosition,
+ endPosition,
+ view_as<float>( { -16.0, -16.0, 0.0 } ),
+ view_as<float>( { 16.0, 16.0, 72.0 } ),
+ MASK_PLAYERSOLID,
+ TraceEntityFilterPlayers,
+ client);
+
+ if (!TR_DidHit(trace))
+ {
+ SetEntityFlags(client, GetEntityFlags(client) & ~FL_ONGROUND);
+ }
+ delete trace;
+}
+
+bool JustNoclipped(int client)
+{
+ return GetGameTickCount() - noclipReleaseTime[client] <= GOKZ_TIMER_START_NOCLIP_TICKS;
+}
+
+void OnClientPutInServer_Noclip(int client)
+{
+ noclipReleaseTime[client] = 0;
+}
+
+// =====[ TURNBINDS ]=====
+
+static int turnbindsLastLeftStart[MAXPLAYERS + 1];
+static int turnbindsLastRightStart[MAXPLAYERS + 1];
+static float turnbindsLastValidYaw[MAXPLAYERS + 1];
+static int turnbindsOldButtons[MAXPLAYERS + 1];
+
+void OnClientPutInServer_Turnbinds(int client)
+{
+ turnbindsLastLeftStart[client] = 0;
+ turnbindsLastRightStart[client] = 0;
+}
+// Ensures that there is a minimum time between starting to turnbind in one direction
+// and then starting to turnbind in the other direction
+void OnPlayerRunCmd_Turnbinds(int client, int buttons, int tickcount, float angles[3])
+{
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+ if (buttons & IN_LEFT && tickcount < turnbindsLastRightStart[client] + RoundToNearest(GOKZ_TURNBIND_COOLDOWN / GetTickInterval()))
+ {
+ angles[1] = turnbindsLastValidYaw[client];
+ TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR);
+ buttons = 0;
+ }
+ else if (buttons & IN_RIGHT && tickcount < turnbindsLastLeftStart[client] + RoundToNearest(GOKZ_TURNBIND_COOLDOWN / GetTickInterval()))
+ {
+ angles[1] = turnbindsLastValidYaw[client];
+ TeleportEntity(client, NULL_VECTOR, angles, NULL_VECTOR);
+ buttons = 0;
+ }
+ else
+ {
+ turnbindsLastValidYaw[client] = angles[1];
+
+ if (!(turnbindsOldButtons[client] & IN_LEFT) && (buttons & IN_LEFT))
+ {
+ turnbindsLastLeftStart[client] = tickcount;
+ }
+
+ if (!(turnbindsOldButtons[client] & IN_RIGHT) && (buttons & IN_RIGHT))
+ {
+ turnbindsLastRightStart[client] = tickcount;
+ }
+
+ turnbindsOldButtons[client] = buttons;
+ }
+}
+
+
+
+// =====[ PLAYER COLLISION ]=====
+
+void OnPlayerSpawn_PlayerCollision(int client)
+{
+ // Let players go through other players
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+}
+
+void OnSetModel_PlayerCollision(int client)
+{
+ // Fix custom models temporarily changing player collisions
+ SetEntPropVector(client, Prop_Data, "m_vecMins", PLAYER_MINS);
+ if (GetEntityFlags(client) & FL_DUCKING == 0)
+ {
+ SetEntPropVector(client, Prop_Data, "m_vecMaxs", PLAYER_MAXS);
+ }
+ else
+ {
+ SetEntPropVector(client, Prop_Data, "m_vecMaxs", PLAYER_MAXS_DUCKED);
+ }
+}
+
+
+// =====[ FORCE SV_FULL_ALLTALK 1 ]=====
+
+void OnRoundStart_ForceAllTalk()
+{
+ gCV_sv_full_alltalk.BoolValue = true;
+}
+
+
+
+// =====[ ERROR SOUNDS ]=====
+
+#define SOUND_ERROR "buttons/button10.wav"
+
+void PlayErrorSound(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_ErrorSounds) == ErrorSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, SOUND_ERROR, _, "Error");
+ }
+}
+
+
+
+// =====[ STOP SOUNDS ]=====
+
+Action OnNormalSound_StopSounds(int entity)
+{
+ char className[20];
+ GetEntityClassname(entity, className, sizeof(className));
+ if (StrEqual(className, "func_button", false))
+ {
+ return Plugin_Handled; // No sounds directly from func_button
+ }
+ return Plugin_Continue;
+}
+
+
+
+// =====[ JOIN TEAM HANDLING ]=====
+
+static bool hasSavedPosition[MAXPLAYERS + 1];
+static float savedOrigin[MAXPLAYERS + 1][3];
+static float savedAngles[MAXPLAYERS + 1][3];
+static bool savedOnLadder[MAXPLAYERS + 1];
+
+void OnClientPutInServer_JoinTeam(int client)
+{
+ // Automatically put the player on a team if he doesn't choose one.
+ // The mp_force_pick_time convar is the built in way to do this, but that obviously
+ // does not call GOKZ_JoinTeam which includes a fix for spawning in the void when
+ // there is no valid spawns available.
+ CreateTimer(12.0, Timer_ForceJoinTeam, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE);
+
+ hasSavedPosition[client] = false;
+}
+
+public Action Timer_ForceJoinTeam(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client))
+ {
+ int team = GetClientTeam(client);
+ if (team == CS_TEAM_NONE)
+ {
+ GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR, false);
+ }
+ }
+ return Plugin_Stop;
+}
+
+void OnTimerStart_JoinTeam(int client)
+{
+ hasSavedPosition[client] = false;
+}
+
+void JoinTeam(int client, int newTeam, bool restorePos, bool forceBroadcast = false)
+{
+ KZPlayer player = KZPlayer(client);
+ int currentTeam = GetClientTeam(client);
+
+ // Don't use CS_TEAM_NONE
+ if (newTeam == CS_TEAM_NONE)
+ {
+ newTeam = CS_TEAM_SPECTATOR;
+ }
+
+ if (newTeam == CS_TEAM_SPECTATOR && currentTeam != CS_TEAM_SPECTATOR)
+ {
+ if (currentTeam != CS_TEAM_NONE)
+ {
+ player.GetOrigin(savedOrigin[client]);
+ player.GetEyeAngles(savedAngles[client]);
+ savedOnLadder[client] = player.Movetype == MOVETYPE_LADDER;
+ hasSavedPosition[client] = true;
+ }
+
+ if (!player.Paused && !player.CanPause)
+ {
+ player.StopTimer();
+ }
+ ChangeClientTeam(client, CS_TEAM_SPECTATOR);
+ Call_GOKZ_OnJoinTeam(client, newTeam);
+ }
+ else if (newTeam == CS_TEAM_CT && currentTeam != CS_TEAM_CT
+ || newTeam == CS_TEAM_T && currentTeam != CS_TEAM_T)
+ {
+ ForcePlayerSuicide(client);
+ CS_SwitchTeam(client, newTeam);
+ CS_RespawnPlayer(client);
+ if (restorePos && hasSavedPosition[client])
+ {
+ TeleportPlayer(client, savedOrigin[client], savedAngles[client]);
+ if (savedOnLadder[client])
+ {
+ player.Movetype = MOVETYPE_LADDER;
+ }
+ }
+ else
+ {
+ player.StopTimer();
+ // Just joining a team alone can put you into weird invalid spawns.
+ // Need to teleport the player to a valid one.
+ float spawnOrigin[3];
+ float spawnAngles[3];
+ GetValidSpawn(spawnOrigin, spawnAngles);
+ TeleportPlayer(client, spawnOrigin, spawnAngles);
+ }
+ hasSavedPosition[client] = false;
+ Call_GOKZ_OnJoinTeam(client, newTeam);
+ }
+ else if (forceBroadcast)
+ {
+ Call_GOKZ_OnJoinTeam(client, newTeam);
+ }
+}
+
+void SendFakeTeamEvent(int client)
+{
+ // Send a fake event to close the team menu
+ Event event = CreateEvent("player_team");
+ event.SetInt("userid", GetClientUserId(client));
+ event.FireToClient(client);
+ event.Cancel();
+}
+
+// =====[ VALID JUMP TRACKING ]=====
+
+/*
+ Valid jump tracking is intended to detect when the player
+ has performed a normal jump that hasn't been affected by
+ (unexpected) teleports or other cases that may result in
+ the player becoming airborne, such as spawning.
+
+ There are ways to trick the plugin, but it is rather
+ unlikely to happen during normal gameplay.
+*/
+
+static bool validJump[MAXPLAYERS + 1];
+static float validJumpTeleportOrigin[MAXPLAYERS + 1][3];
+static int lastInvalidatedTick[MAXPLAYERS + 1];
+bool GetValidJump(int client)
+{
+ return validJump[client];
+}
+
+static void InvalidateJump(int client)
+{
+ lastInvalidatedTick[client] = GetGameTickCount();
+ if (validJump[client])
+ {
+ validJump[client] = false;
+ Call_GOKZ_OnJumpInvalidated(client);
+ }
+}
+
+void OnStopTouchGround_ValidJump(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ if (Movement_GetMovetype(client) == MOVETYPE_WALK && lastInvalidatedTick[client] != GetGameTickCount())
+ {
+ validJump[client] = true;
+ Call_GOKZ_OnJumpValidated(client, jumped, ladderJump, jumpbug);
+ }
+ else
+ {
+ InvalidateJump(client);
+ }
+}
+
+void OnPlayerRunCmdPost_ValidJump(int client)
+{
+ if (gB_VelocityTeleported[client] || gB_OriginTeleported[client])
+ {
+ InvalidateJump(client);
+ }
+}
+
+void OnChangeMovetype_ValidJump(int client, MoveType oldMovetype, MoveType newMovetype)
+{
+ if (oldMovetype == MOVETYPE_LADDER && newMovetype == MOVETYPE_WALK && lastInvalidatedTick[client] != GetGameTickCount()) // Ladderjump
+ {
+ validJump[client] = true;
+ Call_GOKZ_OnJumpValidated(client, false, true, false);
+ }
+ else
+ {
+ InvalidateJump(client);
+ }
+}
+
+void OnClientDisconnect_ValidJump(int client)
+{
+ InvalidateJump(client);
+}
+
+void OnPlayerSpawn_ValidJump(int client)
+{
+ // That should definitely be out of bounds
+ CopyVector({ 40000.0, 40000.0, 40000.0 }, validJumpTeleportOrigin[client]);
+ InvalidateJump(client);
+}
+
+void OnPlayerDeath_ValidJump(int client)
+{
+ InvalidateJump(client);
+}
+
+void OnValidOriginChange_ValidJump(int client, const float origin[3])
+{
+ CopyVector(origin, validJumpTeleportOrigin[client]);
+}
+
+void OnTeleport_ValidJump(int client)
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ if (gB_OriginTeleported[client] && GetVectorDistance(validJumpTeleportOrigin[client], origin, true) <= EPSILON)
+ {
+ gB_OriginTeleported[client] = false;
+ CopyVector({ 40000.0, 40000.0, 40000.0 }, validJumpTeleportOrigin[client]);
+ return;
+ }
+
+ if (gB_OriginTeleported[client])
+ {
+ InvalidateJump(client);
+ Call_GOKZ_OnTeleport(client);
+ }
+
+ if (gB_VelocityTeleported[client])
+ {
+ InvalidateJump(client);
+ }
+}
+
+
+
+// =====[ FIRST SPAWN ]=====
+
+static bool hasSpawned[MAXPLAYERS + 1];
+
+void OnClientPutInServer_FirstSpawn(int client)
+{
+ hasSpawned[client] = false;
+}
+
+void OnPlayerSpawn_FirstSpawn(int client)
+{
+ int team = GetClientTeam(client);
+ if (!hasSpawned[client] && (team == CS_TEAM_CT || team == CS_TEAM_T))
+ {
+ hasSpawned[client] = true;
+ Call_GOKZ_OnFirstSpawn(client);
+ }
+}
+
+
+
+// =====[ TIME LIMIT ]=====
+
+void OnConfigsExecuted_TimeLimit()
+{
+ CreateTimer(1.0, Timer_TimeLimit, _, TIMER_REPEAT);
+}
+
+public Action Timer_TimeLimit(Handle timer)
+{
+ int timelimit;
+ if (!GetMapTimeLimit(timelimit) || timelimit == 0)
+ {
+ return Plugin_Continue;
+ }
+
+ int timeleft;
+ // Check for less than -1 in case we miss 0 - ignore -1 because that means infinite time limit
+ if (GetMapTimeLeft(timeleft) && (timeleft == 0 || timeleft < -1))
+ {
+ CreateTimer(5.0, Timer_EndRound); // End the round after a delay or it won't end the map
+ return Plugin_Stop;
+ }
+
+ return Plugin_Continue;
+}
+
+public Action Timer_EndRound(Handle timer)
+{
+ CS_TerminateRound(1.0, CSRoundEnd_Draw, true);
+ return Plugin_Continue;
+}
+
+
+
+// =====[ COURSE REGISTER ]=====
+
+static bool startRegistered[GOKZ_MAX_COURSES];
+static bool endRegistered[GOKZ_MAX_COURSES];
+static bool courseRegistered[GOKZ_MAX_COURSES];
+
+bool GetCourseRegistered(int course)
+{
+ return courseRegistered[course];
+}
+
+void RegisterCourseStart(int course)
+{
+ startRegistered[course] = true;
+ TryRegisterCourse(course);
+}
+
+void RegisterCourseEnd(int course)
+{
+ endRegistered[course] = true;
+ TryRegisterCourse(course);
+}
+
+void OnMapStart_CourseRegister()
+{
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ courseRegistered[course] = false;
+ }
+}
+
+static void TryRegisterCourse(int course)
+{
+ if (!courseRegistered[course] && startRegistered[course] && endRegistered[course])
+ {
+ courseRegistered[course] = true;
+ Call_GOKZ_OnCourseRegistered(course);
+ }
+}
+
+
+
+// =====[ SPAWN FIXES ]=====
+
+void OnMapStart_FixMissingSpawns()
+{
+ int tSpawn = FindEntityByClassname(-1, "info_player_terrorist");
+ int ctSpawn = FindEntityByClassname(-1, "info_player_counterterrorist");
+
+ if (tSpawn == -1 && ctSpawn == -1)
+ {
+ LogMessage("Couldn't fix spawns because none exist.");
+ return;
+ }
+
+ if (tSpawn == -1 || ctSpawn == -1)
+ {
+ float origin[3], angles[3];
+ GetValidSpawn(origin, angles);
+
+ int newSpawn = CreateEntityByName((tSpawn == -1) ? "info_player_terrorist" : "info_player_counterterrorist");
+ if (DispatchSpawn(newSpawn))
+ {
+ TeleportEntity(newSpawn, origin, angles, NULL_VECTOR);
+ }
+ }
+}
+
+// =====[ BUTTONS ]=====
+
+void OnClientPreThinkPost_UseButtons(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_ButtonThroughPlayers) == ButtonThroughPlayers_Enabled && GetEntProp(client, Prop_Data, "m_afButtonPressed") & IN_USE)
+ {
+ int entity = FindUseEntity(client);
+ if (entity != -1)
+ {
+ AcceptEntityInput(entity, "Use", client, client, 1);
+ }
+ }
+}
+
+static int FindUseEntity(int client)
+{
+ float fwd[3];
+ float angles[3];
+ GetClientEyeAngles(client, angles);
+ GetAngleVectors(angles, fwd, NULL_VECTOR, NULL_VECTOR);
+
+ Handle trace;
+
+ float eyeOrigin[3];
+ GetClientEyePosition(client, eyeOrigin);
+ int useableContents = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE;
+
+ float endpos[3];
+
+ // Check if +use trace collide with a player first, so we don't activate any button twice
+ trace = TR_TraceRayFilterEx(eyeOrigin, angles, useableContents, RayType_Infinite, TRFOtherPlayersOnly, client);
+ if (TR_DidHit(trace))
+ {
+ int ent = TR_GetEntityIndex(trace);
+ if (ent < 1 || ent > MaxClients)
+ {
+ return -1;
+ }
+ // Search for a button behind it.
+ trace = TR_TraceRayFilterEx(eyeOrigin, angles, useableContents, RayType_Infinite, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ char buffer[20];
+ ent = TR_GetEntityIndex(trace);
+ // Make sure that it is a button, and this button activates when pressed.
+ // If it is not a button, check its parent to see if it is a button.
+ bool isButton;
+ while (ent != -1)
+ {
+ GetEntityClassname(ent, buffer, sizeof(buffer));
+ if (StrEqual("func_button", buffer, false) && GetEntProp(ent, Prop_Data, "m_spawnflags") & SF_BUTTON_USE_ACTIVATES)
+ {
+ isButton = true;
+ break;
+ }
+ else
+ {
+ ent = GetEntPropEnt(ent, Prop_Data, "m_hMoveParent");
+ }
+ }
+ if (isButton)
+ {
+ TR_GetEndPosition(endpos, trace);
+ float delta[3];
+ for (int i = 0; i < 2; i++)
+ {
+ delta[i] = endpos[i] - eyeOrigin[i];
+ }
+ // Z distance is treated differently.
+ float m_vecMins[3];
+ float m_vecMaxs[3];
+ float m_vecOrigin[3];
+ GetEntPropVector(ent, Prop_Send, "m_vecOrigin", m_vecOrigin);
+ GetEntPropVector(ent, Prop_Send, "m_vecMins", m_vecMins);
+ GetEntPropVector(ent, Prop_Send, "m_vecMaxs", m_vecMaxs);
+
+ delta[2] = IntervalDistance(endpos[2], m_vecOrigin[2] + m_vecMins[2], m_vecOrigin[2] + m_vecMaxs[2]);
+ if (GetVectorLength(delta) < 80.0)
+ {
+ return ent;
+ }
+ }
+ }
+ }
+
+ int nearestEntity;
+ float nearestPoint[3];
+ float nearestDist = FLOAT_MAX;
+ ArrayList entities = new ArrayList();
+ TR_EnumerateEntitiesSphere(eyeOrigin, 80.0, 1<<5, AddEntities, entities);
+ for (int i = 0; i < entities.Length; i++)
+ {
+ char buffer[64];
+ int ent = entities.Get(i);
+ GetEntityClassname(ent, buffer, sizeof(buffer));
+ // Check if the entity is a button and it is pressable.
+ if (StrEqual("func_button", buffer, false) && GetEntProp(ent, Prop_Data, "m_spawnflags") & SF_BUTTON_USE_ACTIVATES)
+ {
+ float point[3];
+ CalcNearestPoint(ent, eyeOrigin, point);
+
+ float dir[3];
+ for (int j = 0; j < 3; j++)
+ {
+ dir[j] = point[j] - eyeOrigin[2];
+ }
+ // Check the maximum angle the player can be away from the button.
+ float minimumDot = GetEntPropFloat(ent, Prop_Send, "m_flUseLookAtAngle");
+ NormalizeVector(dir, dir);
+ float dot = GetVectorDotProduct(dir, fwd);
+ if (dot < minimumDot)
+ {
+ continue;
+ }
+
+ float dist = CalcDistanceToLine(point, eyeOrigin, fwd);
+ if (dist < nearestDist)
+ {
+ trace = TR_TraceRayFilterEx(eyeOrigin, point, useableContents, RayType_EndPoint, TraceEntityFilterPlayers);
+ if (TR_GetFraction(trace) == 1.0 || TR_GetEntityIndex(trace) == ent)
+ {
+ CopyVector(point, nearestPoint);
+ nearestDist = dist;
+ nearestEntity = ent;
+ }
+ }
+ }
+ }
+ // We found the closest button, but we still need to check if there is a player in front of it or not.
+ // In the case that there isn't a player inbetween, we don't return the entity index, because that button will be pressed by the game function anyway.
+ // If there is, we will press two buttons at once, the "right" button found by this function and the "wrong" button that we only happen to press because
+ // there is a player in the way.
+
+ trace = TR_TraceRayFilterEx(eyeOrigin, nearestPoint, useableContents, RayType_EndPoint, TRFOtherPlayersOnly);
+ if (TR_DidHit(trace))
+ {
+ return nearestEntity;
+ }
+ return -1;
+}
+
+public bool AddEntities(int entity, ArrayList entities)
+{
+ entities.Push(entity);
+ return true;
+}
+
+static float IntervalDistance(float x, float x0, float x1)
+{
+ if (x0 > x1)
+ {
+ float tmp = x0;
+ x0 = x1;
+ x1 = tmp;
+ }
+ if (x < x0)
+ {
+ return x0 - x;
+ }
+ else if (x > x1)
+ {
+ return x - x1;
+ }
+ return 0.0;
+}
+// TraceRay filter for other players exclusively.
+public bool TRFOtherPlayersOnly(int entity, int contentmask, int client)
+{
+ return (0 < entity <= MaxClients) && (entity != client);
+}
+
+// =====[ SAFE MODE ]=====
+
+void ToggleNubSafeGuard(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledNUB)
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_Disabled);
+ }
+ else
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_EnabledNUB);
+ }
+}
+
+void ToggleProSafeGuard(int client)
+{
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO)
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_Disabled);
+ }
+ else
+ {
+ GOKZ_SetCoreOption(client, Option_Safeguard, Safeguard_EnabledPRO);
+ }
+}
diff --git a/sourcemod/scripting/gokz-core/modes.sp b/sourcemod/scripting/gokz-core/modes.sp
new file mode 100644
index 0000000..ce6f8ae
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/modes.sp
@@ -0,0 +1,106 @@
+static bool modeLoaded[MODE_COUNT];
+static int modeVersion[MODE_COUNT];
+static bool GOKZHitPerf[MAXPLAYERS + 1];
+static float GOKZTakeoffSpeed[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetModeLoaded(int mode)
+{
+ return modeLoaded[mode];
+}
+
+int GetModeVersion(int mode)
+{
+ return modeLoaded[mode] ? modeVersion[mode] : -1;
+}
+
+void SetModeLoaded(int mode, bool loaded, int version = -1)
+{
+ if (!modeLoaded[mode] && loaded)
+ {
+ modeLoaded[mode] = true;
+ modeVersion[mode] = version;
+ Call_GOKZ_OnModeLoaded(mode);
+ }
+ else if (modeLoaded[mode] && !loaded)
+ {
+ modeLoaded[mode] = false;
+ Call_GOKZ_OnModeUnloaded(mode);
+ }
+}
+
+int GetLoadedModeCount()
+{
+ int count = 0;
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ if (modeLoaded[mode])
+ {
+ count++;
+ }
+ }
+ return count;
+}
+
+int GetALoadedMode()
+{
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ if (GOKZ_GetModeLoaded(mode))
+ {
+ return mode;
+ }
+ }
+ return -1; // Uh-oh
+}
+
+bool GetGOKZHitPerf(int client)
+{
+ return GOKZHitPerf[client];
+}
+
+void SetGOKZHitPerf(int client, bool hitPerf)
+{
+ GOKZHitPerf[client] = hitPerf;
+}
+
+float GetGOKZTakeoffSpeed(int client)
+{
+ return GOKZTakeoffSpeed[client];
+}
+
+void SetGOKZTakeoffSpeed(int client, float takeoffSpeed)
+{
+ GOKZTakeoffSpeed[client] = takeoffSpeed;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnAllPluginsLoaded_Modes()
+{
+ if (GetLoadedModeCount() <= 0)
+ {
+ SetFailState("At least one GOKZ mode plugin is required.");
+ }
+}
+
+void OnPlayerSpawn_Modes(int client)
+{
+ GOKZHitPerf[client] = false;
+ GOKZTakeoffSpeed[client] = 0.0;
+}
+
+void OnOptionChanged_Mode(int client, Option option)
+{
+ if (option == Option_Mode)
+ {
+ // Remove speed when switching modes
+ Movement_SetVelocityModifier(client, 1.0);
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/natives.sp b/sourcemod/scripting/gokz-core/natives.sp
new file mode 100644
index 0000000..319810c
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/natives.sp
@@ -0,0 +1,647 @@
+void CreateNatives()
+{
+ CreateNative("GOKZ_GetModeLoaded", Native_GetModeLoaded);
+ CreateNative("GOKZ_GetModeVersion", Native_GetModeVersion);
+ CreateNative("GOKZ_SetModeLoaded", Native_SetModeLoaded);
+ CreateNative("GOKZ_GetLoadedModeCount", Native_GetLoadedModeCount);
+ CreateNative("GOKZ_SetMode", Native_SetMode);
+ CreateNative("GOKZ_PrintToChat", Native_PrintToChat);
+ CreateNative("GOKZ_PrintToChatAndLog", Native_PrintToChatAndLog);
+ CreateNative("GOKZ_GetOptionsTopMenu", Native_GetOptionsTopMenu);
+ CreateNative("GOKZ_GetCourseRegistered", Native_GetCourseRegistered);
+
+ CreateNative("GOKZ_StartTimer", Native_StartTimer);
+ CreateNative("GOKZ_EndTimer", Native_EndTimer);
+ CreateNative("GOKZ_StopTimer", Native_StopTimer);
+ CreateNative("GOKZ_StopTimerAll", Native_StopTimerAll);
+ CreateNative("GOKZ_TeleportToStart", Native_TeleportToStart);
+ CreateNative("GOKZ_TeleportToSearchStart", Native_TeleportToSearchStart);
+ CreateNative("GOKZ_GetVirtualButtonPosition", Native_GetVirtualButtonPosition);
+ CreateNative("GOKZ_SetVirtualButtonPosition", Native_SetVirtualButtonPosition);
+ CreateNative("GOKZ_ResetVirtualButtonPosition", Native_ResetVirtualButtonPosition);
+ CreateNative("GOKZ_LockVirtualButtons", Native_LockVirtualButtons);
+ CreateNative("GOKZ_GetStartPosition", Native_GetStartPosition);
+ CreateNative("GOKZ_SetStartPosition", Native_SetStartPosition);
+ CreateNative("GOKZ_TeleportToEnd", Native_TeleportToEnd);
+ CreateNative("GOKZ_GetStartPositionType", Native_GetStartPositionType);
+ CreateNative("GOKZ_SetStartPositionToMapStart", Native_SetStartPositionToMapStart);
+ CreateNative("GOKZ_MakeCheckpoint", Native_MakeCheckpoint);
+ CreateNative("GOKZ_GetCanMakeCheckpoint", Native_GetCanMakeCheckpoint);
+ CreateNative("GOKZ_TeleportToCheckpoint", Native_TeleportToCheckpoint);
+ CreateNative("GOKZ_GetCanTeleportToCheckpoint", Native_GetCanTeleportToCheckpoint);
+ CreateNative("GOKZ_PrevCheckpoint", Native_PrevCheckpoint);
+ CreateNative("GOKZ_GetCanPrevCheckpoint", Native_GetCanPrevCheckpoint);
+ CreateNative("GOKZ_NextCheckpoint", Native_NextCheckpoint);
+ CreateNative("GOKZ_GetCanNextCheckpoint", Native_GetCanNextCheckpoint);
+ CreateNative("GOKZ_UndoTeleport", Native_UndoTeleport);
+ CreateNative("GOKZ_GetCanUndoTeleport", Native_GetCanUndoTeleport);
+ CreateNative("GOKZ_Pause", Native_Pause);
+ CreateNative("GOKZ_GetCanPause", Native_GetCanPause);
+ CreateNative("GOKZ_Resume", Native_Resume);
+ CreateNative("GOKZ_GetCanResume", Native_GetCanResume);
+ CreateNative("GOKZ_TogglePause", Native_TogglePause);
+ CreateNative("GOKZ_GetCanTeleportToStart", Native_GetCanTeleportToStart);
+ CreateNative("GOKZ_GetCanTeleportToEnd", Native_GetCanTeleportToEnd);
+ CreateNative("GOKZ_PlayErrorSound", Native_PlayErrorSound);
+ CreateNative("GOKZ_SetValidJumpOrigin", Native_SetValidJumpOrigin);
+
+ CreateNative("GOKZ_GetTimerRunning", Native_GetTimerRunning);
+ CreateNative("GOKZ_GetValidTimer", Native_GetValidTimer);
+ CreateNative("GOKZ_GetCourse", Native_GetCourse);
+ CreateNative("GOKZ_SetCourse", Native_SetCourse);
+ CreateNative("GOKZ_GetPaused", Native_GetPaused);
+ CreateNative("GOKZ_GetTime", Native_GetTime);
+ CreateNative("GOKZ_SetTime", Native_SetTime);
+ CreateNative("GOKZ_InvalidateRun", Native_InvalidateRun);
+ CreateNative("GOKZ_GetCheckpointCount", Native_GetCheckpointCount);
+ CreateNative("GOKZ_SetCheckpointCount", Native_SetCheckpointCount);
+ CreateNative("GOKZ_GetCheckpointData", Native_GetCheckpointData);
+ CreateNative("GOKZ_SetCheckpointData", Native_SetCheckpointData);
+ CreateNative("GOKZ_GetUndoTeleportData", Native_GetUndoTeleportData);
+ CreateNative("GOKZ_SetUndoTeleportData", Native_SetUndoTeleportData);
+ CreateNative("GOKZ_GetTeleportCount", Native_GetTeleportCount);
+ CreateNative("GOKZ_SetTeleportCount", Native_SetTeleportCount);
+ CreateNative("GOKZ_RegisterOption", Native_RegisterOption);
+ CreateNative("GOKZ_GetOptionProp", Native_GetOptionProp);
+ CreateNative("GOKZ_SetOptionProp", Native_SetOptionProp);
+ CreateNative("GOKZ_GetOption", Native_GetOption);
+ CreateNative("GOKZ_SetOption", Native_SetOption);
+ CreateNative("GOKZ_GetHitPerf", Native_GetHitPerf);
+ CreateNative("GOKZ_SetHitPerf", Native_SetHitPerf);
+ CreateNative("GOKZ_GetTakeoffSpeed", Native_GetTakeoffSpeed);
+ CreateNative("GOKZ_SetTakeoffSpeed", Native_SetTakeoffSpeed);
+ CreateNative("GOKZ_GetValidJump", Native_GetValidJump);
+ CreateNative("GOKZ_JoinTeam", Native_JoinTeam);
+
+ CreateNative("GOKZ_EmitSoundToClient", Native_EmitSoundToClient);
+}
+
+public int Native_GetModeLoaded(Handle plugin, int numParams)
+{
+ return view_as<int>(GetModeLoaded(GetNativeCell(1)));
+}
+
+public int Native_GetModeVersion(Handle plugin, int numParams)
+{
+ return view_as<int>(GetModeVersion(GetNativeCell(1)));
+}
+
+public int Native_SetModeLoaded(Handle plugin, int numParams)
+{
+ SetModeLoaded(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3));
+ return 0;
+}
+
+public int Native_GetLoadedModeCount(Handle plugin, int numParams)
+{
+ return GetLoadedModeCount();
+}
+
+public int Native_SetMode(Handle plugin, int numParams)
+{
+ return view_as<bool>(SwitchToModeIfAvailable(GetNativeCell(1),GetNativeCell(2)));
+}
+
+public int Native_PrintToChatAndLog(Handle plugin, int numParams)
+{
+ NativeHelper_PrintToChatOrLog(true);
+ return 0;
+}
+
+public int Native_PrintToChat(Handle plugin, int numParams)
+{
+ NativeHelper_PrintToChatOrLog(false);
+ return 0;
+}
+
+static int NativeHelper_PrintToChatOrLog(bool alwaysLog)
+{
+ int client = GetNativeCell(1);
+ bool addPrefix = GetNativeCell(2);
+
+ char buffer[1024];
+ SetGlobalTransTarget(client);
+ FormatNativeString(0, 3, 4, sizeof(buffer), _, buffer);
+
+ // The console (client 0) gets a special treatment
+ if (client == 0 || (!IsValidClient(client) && !IsClientSourceTV(client)) || alwaysLog)
+ {
+ // Strip colors
+ // We can't regex-replace, so I'm quite sure that's the most efficient way.
+ // It's also not perfectly safe, we will just assume you never have curly
+ // braces without a color in beween.
+ char colorlessBuffer[1024];
+ FormatEx(colorlessBuffer, sizeof(colorlessBuffer), "%L: ", client);
+ int iIn = 0, iOut = strlen(colorlessBuffer);
+ do
+ {
+ if (buffer[iIn] == '{')
+ {
+ for (; buffer[iIn] != '}' && iIn < sizeof(buffer) - 2; iIn++){}
+ if (iIn >= sizeof(buffer) - 2)
+ {
+ break;
+ }
+ iIn++;
+ continue;
+ }
+
+ colorlessBuffer[iOut] = buffer[iIn];
+ iIn++;
+ iOut++;
+ } while (buffer[iIn] != '\0' && iIn < sizeof(buffer) - 1 && iOut < sizeof(colorlessBuffer) - 1);
+ colorlessBuffer[iOut] = '\0';
+ LogMessage(colorlessBuffer);
+ }
+
+ if (client != 0)
+ {
+ if (addPrefix)
+ {
+ char prefix[64];
+ gCV_gokz_chat_prefix.GetString(prefix, sizeof(prefix));
+ Format(buffer, sizeof(buffer), "%s%s", prefix, buffer);
+ }
+
+ CPrintToChat(client, "%s", buffer);
+ }
+ return 0;
+}
+
+public int Native_GetOptionsTopMenu(Handle plugin, int numParams)
+{
+ return view_as<int>(GetOptionsTopMenu());
+}
+
+public int Native_GetCourseRegistered(Handle plugin, int numParams)
+{
+ return view_as<int>(GetCourseRegistered(GetNativeCell(1)));
+}
+
+public int Native_StartTimer(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ return view_as<int>(TimerStart(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3)));
+}
+
+public int Native_EndTimer(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ return view_as<int>(TimerEnd(GetNativeCell(1), GetNativeCell(2)));
+}
+
+public int Native_StopTimer(Handle plugin, int numParams)
+{
+ return view_as<int>(TimerStop(GetNativeCell(1), GetNativeCell(2)));
+}
+
+public int Native_StopTimerAll(Handle plugin, int numParams)
+{
+ TimerStopAll(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_TeleportToStart(Handle plugin, int numParams)
+{
+ TeleportToStart(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_TeleportToSearchStart(Handle plugin, int numParams)
+{
+ TeleportToSearchStart(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
+
+public int Native_GetVirtualButtonPosition(Handle plugin, int numParams)
+{
+ int course;
+ float position[3];
+
+ course = GetVirtualButtonPosition(GetNativeCell(1), position, GetNativeCell(3));
+ SetNativeArray(2, position, sizeof(position));
+
+ return course;
+}
+
+public int Native_SetVirtualButtonPosition(Handle plugin, int numParams)
+{
+ float position[3];
+
+ GetNativeArray(2, position, sizeof(position));
+ SetVirtualButtonPosition(GetNativeCell(1), position, GetNativeCell(3), view_as<bool>(GetNativeCell(4)));
+ return 0;
+}
+
+public int Native_ResetVirtualButtonPosition(Handle plugin, int numParams)
+{
+ ResetVirtualButtonPosition(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
+
+public int Native_LockVirtualButtons(Handle plugin, int numParams)
+{
+ LockVirtualButtons(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetStartPosition(Handle plugin, int numParams)
+{
+ StartPositionType type;
+ float position[3], angles[3];
+
+ type = GetStartPosition(GetNativeCell(1), position, angles);
+ SetNativeArray(2, position, sizeof(position));
+ SetNativeArray(3, angles, sizeof(angles));
+
+ return view_as<int>(type);
+}
+
+public int Native_SetStartPosition(Handle plugin, int numParams)
+{
+ float position[3], angles[3];
+
+ GetNativeArray(3, position, sizeof(position));
+ GetNativeArray(4, angles, sizeof(angles));
+ SetStartPosition(GetNativeCell(1), GetNativeCell(2), position, angles);
+ return 0;
+}
+
+public int Native_TeleportToEnd(Handle plugin, int numParams)
+{
+ TeleportToEnd(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
+
+public int Native_GetStartPositionType(Handle plugin, int numParams)
+{
+ return view_as<int>(GetStartPositionType(GetNativeCell(1)));
+}
+
+public int Native_SetStartPositionToMapStart(Handle plugin, int numParams)
+{
+ return SetStartPositionToMapStart(GetNativeCell(1), GetNativeCell(2));
+}
+
+public int Native_MakeCheckpoint(Handle plugin, int numParams)
+{
+ MakeCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanMakeCheckpoint(Handle plugin, int numParams)
+{
+ return CanMakeCheckpoint(GetNativeCell(1));
+}
+
+public int Native_TeleportToCheckpoint(Handle plugin, int numParams)
+{
+ TeleportToCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanTeleportToCheckpoint(Handle plugin, int numParams)
+{
+ return CanTeleportToCheckpoint(GetNativeCell(1));
+}
+
+public int Native_PrevCheckpoint(Handle plugin, int numParams)
+{
+ PrevCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanPrevCheckpoint(Handle plugin, int numParams)
+{
+ return CanPrevCheckpoint(GetNativeCell(1));
+}
+
+public int Native_NextCheckpoint(Handle plugin, int numParams)
+{
+ NextCheckpoint(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanNextCheckpoint(Handle plugin, int numParams)
+{
+ return CanNextCheckpoint(GetNativeCell(1));
+}
+
+public int Native_UndoTeleport(Handle plugin, int numParams)
+{
+ UndoTeleport(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanUndoTeleport(Handle plugin, int numParams)
+{
+ return CanUndoTeleport(GetNativeCell(1));
+}
+
+public int Native_Pause(Handle plugin, int numParams)
+{
+ Pause(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanPause(Handle plugin, int numParams)
+{
+ return CanPause(GetNativeCell(1));
+}
+
+public int Native_Resume(Handle plugin, int numParams)
+{
+ Resume(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanResume(Handle plugin, int numParams)
+{
+ return CanResume(GetNativeCell(1));
+}
+
+public int Native_TogglePause(Handle plugin, int numParams)
+{
+ TogglePause(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetCanTeleportToStart(Handle plugin, int numParams)
+{
+ return CanTeleportToStart(GetNativeCell(1));
+}
+
+public int Native_GetCanTeleportToEnd(Handle plugin, int numParams)
+{
+ return CanTeleportToEnd(GetNativeCell(1));
+}
+
+public int Native_PlayErrorSound(Handle plugin, int numParams)
+{
+ PlayErrorSound(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_SetValidJumpOrigin(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+ float origin[3];
+ GetNativeArray(2, origin, sizeof(origin));
+
+ // The order is important here!
+ OnValidOriginChange_ValidJump(client, origin);
+
+ // Using Movement_SetOrigin instead causes considerable lag for spectators
+ SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+ return 0;
+}
+
+public int Native_GetTimerRunning(Handle plugin, int numParams)
+{
+ return view_as<int>(GetTimerRunning(GetNativeCell(1)));
+}
+
+public int Native_GetValidTimer(Handle plugin, int numParams)
+{
+ return view_as<int>(GetValidTimer(GetNativeCell(1)));
+}
+
+public int Native_GetCourse(Handle plugin, int numParams)
+{
+ return GetCurrentCourse(GetNativeCell(1));
+}
+
+public int Native_SetCourse(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ SetCurrentCourse(GetNativeCell(1), GetNativeCell(2));
+ return view_as<int>(false);
+}
+
+public int Native_GetPaused(Handle plugin, int numParams)
+{
+ return view_as<int>(GetPaused(GetNativeCell(1)));
+}
+
+public int Native_GetTime(Handle plugin, int numParams)
+{
+ return view_as<int>(GetCurrentTime(GetNativeCell(1)));
+}
+
+public int Native_SetTime(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ SetCurrentTime(GetNativeCell(1), view_as<float>(GetNativeCell(2)));
+ return view_as<int>(true);
+}
+
+public int Native_InvalidateRun(Handle plugin, int numParams)
+{
+ InvalidateRun(GetNativeCell(1));
+ return view_as<int>(true);
+}
+
+public int Native_GetCheckpointCount(Handle plugin, int numParams)
+{
+ return GetCheckpointCount(GetNativeCell(1));
+}
+
+public int Native_SetCheckpointCount(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ SetCheckpointCount(GetNativeCell(1), GetNativeCell(2));
+ return view_as<int>(true);
+}
+
+public int Native_GetCheckpointData(Handle plugin, int numParams)
+{
+ ArrayList temp = GetCheckpointData(GetNativeCell(1));
+ Handle cps = CloneHandle(temp, plugin);
+ delete temp;
+ return view_as<int>(cps);
+}
+
+public int Native_SetCheckpointData(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ return SetCheckpointData(GetNativeCell(1), view_as<ArrayList>(GetNativeCell(2)), GetNativeCell(3));
+}
+
+public int Native_GetUndoTeleportData(Handle plugin, int numParams)
+{
+ ArrayList temp = GetUndoTeleportData(GetNativeCell(1));
+ Handle utd = CloneHandle(temp, plugin);
+ delete temp;
+ return view_as<int>(utd);
+}
+
+public int Native_SetUndoTeleportData(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+ return SetUndoTeleportData(GetNativeCell(1), view_as<ArrayList>(GetNativeCell(2)), GetNativeCell(3));
+}
+
+public int Native_GetTeleportCount(Handle plugin, int numParams)
+{
+ return GetTeleportCount(GetNativeCell(1));
+}
+
+public int Native_SetTeleportCount(Handle plugin, int numParams)
+{
+ if (BlockedExternallyCalledTimerNative(plugin, GetNativeCell(1)))
+ {
+ return view_as<int>(false);
+ }
+
+ SetTeleportCount(GetNativeCell(1), GetNativeCell(2));
+ return view_as<int>(true);
+}
+
+public int Native_RegisterOption(Handle plugin, int numParams)
+{
+ char name[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(1, name, sizeof(name));
+ char description[255];
+ GetNativeString(2, description, sizeof(description));
+ return view_as<int>(RegisterOption(name, description, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), GetNativeCell(6)));
+}
+
+public int Native_GetOptionProp(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(1, option, sizeof(option));
+ OptionProp prop = GetNativeCell(2);
+ any value = GetOptionProp(option, prop);
+
+ // Return clone of Handle if called by another plugin
+ if (prop == OptionProp_Cookie && plugin != gH_ThisPlugin)
+ {
+ value = CloneHandle(value, plugin);
+ }
+
+ return value;
+}
+
+public int Native_SetOptionProp(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(1, option, sizeof(option));
+ OptionProp prop = GetNativeCell(2);
+ return SetOptionProp(option, prop, GetNativeCell(3));
+}
+
+public int Native_GetOption(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(2, option, sizeof(option));
+ return view_as<int>(GetOption(GetNativeCell(1), option));
+}
+
+public int Native_SetOption(Handle plugin, int numParams)
+{
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ GetNativeString(2, option, sizeof(option));
+ return view_as<int>(SetOption(GetNativeCell(1), option, GetNativeCell(3)));
+}
+
+public int Native_GetHitPerf(Handle plugin, int numParams)
+{
+ return view_as<int>(GetGOKZHitPerf(GetNativeCell(1)));
+}
+
+public int Native_SetHitPerf(Handle plugin, int numParams)
+{
+ SetGOKZHitPerf(GetNativeCell(1), view_as<bool>(GetNativeCell(2)));
+ return 0;
+}
+
+public int Native_GetTakeoffSpeed(Handle plugin, int numParams)
+{
+ return view_as<int>(GetGOKZTakeoffSpeed(GetNativeCell(1)));
+}
+
+public int Native_SetTakeoffSpeed(Handle plugin, int numParams)
+{
+ SetGOKZTakeoffSpeed(GetNativeCell(1), view_as<float>(GetNativeCell(2)));
+ return 0;
+}
+
+public int Native_GetValidJump(Handle plugin, int numParams)
+{
+ return view_as<int>(GetValidJump(GetNativeCell(1)));
+}
+
+public int Native_JoinTeam(Handle plugin, int numParams)
+{
+ JoinTeam(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3), GetNativeCell(4));
+ return 0;
+}
+
+public int Native_EmitSoundToClient(Handle plugin, int numParams)
+{
+ int client = GetNativeCell(1);
+
+ char sample[PLATFORM_MAX_PATH];
+ GetNativeString(2, sample, sizeof(sample));
+
+ float volume = GetNativeCell(3);
+ float newVolume = volume;
+
+ char description[64];
+ GetNativeString(4, description, sizeof(description));
+
+ Action result;
+
+ Call_GOKZ_OnEmitSoundToClient(client, sample, newVolume, description, result);
+ if (result == Plugin_Stop)
+ {
+ return 0;
+ }
+ if (result == Plugin_Changed)
+ {
+ EmitSoundToClient(client, sample, _, _, _, _, newVolume);
+ return 0;
+ }
+ EmitSoundToClient(client, sample, _, _, _, _, volume);
+ return 0;
+}
+
+// =====[ PRIVATE ]=====
+
+static bool BlockedExternallyCalledTimerNative(Handle plugin, int client)
+{
+ if (plugin != gH_ThisPlugin)
+ {
+ Action result;
+ Call_GOKZ_OnTimerNativeCalledExternally(plugin, client, result);
+ if (result != Plugin_Continue)
+ {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/sourcemod/scripting/gokz-core/options.sp b/sourcemod/scripting/gokz-core/options.sp
new file mode 100644
index 0000000..6daef13
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/options.sp
@@ -0,0 +1,438 @@
+static StringMap optionData;
+static StringMap optionDescriptions;
+
+
+
+// =====[ PUBLIC ]=====
+
+bool RegisterOption(const char[] name, const char[] description, OptionType type, any defaultValue, any minValue, any maxValue)
+{
+ if (!IsValueInRange(type, defaultValue, minValue, maxValue))
+ {
+ LogError("Failed to register option \"%s\" due to invalid default value and value range.", name);
+ return false;
+ }
+
+ if (strlen(name) > GOKZ_OPTION_MAX_NAME_LENGTH - 1)
+ {
+ LogError("Failed to register option \"%s\" because its name is too long.", name);
+ return false;
+ }
+
+ if (strlen(name) > GOKZ_OPTION_MAX_NAME_LENGTH - 1)
+ {
+ LogError("Failed to register option \"%s\" because its description is too long.", name);
+ return false;
+ }
+
+ ArrayList data;
+ Cookie cookie;
+ if (IsRegisteredOption(name))
+ {
+ optionData.GetValue(name, data);
+ cookie = GetOptionProp(name, OptionProp_Cookie);
+ }
+ else
+ {
+ data = new ArrayList(1, view_as<int>(OPTIONPROP_COUNT));
+ cookie = new Cookie(name, description, CookieAccess_Private);
+ }
+
+ data.Set(view_as<int>(OptionProp_Cookie), cookie);
+ data.Set(view_as<int>(OptionProp_Type), type);
+ data.Set(view_as<int>(OptionProp_DefaultValue), defaultValue);
+ data.Set(view_as<int>(OptionProp_MinValue), minValue);
+ data.Set(view_as<int>(OptionProp_MaxValue), maxValue);
+
+ optionData.SetValue(name, data, true);
+ optionDescriptions.SetString(name, description, true);
+
+ // Support late-loading/registering
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (AreClientCookiesCached(client))
+ {
+ LoadOption(client, name);
+ }
+ }
+
+ return true;
+}
+
+any GetOptionProp(const char[] option, OptionProp prop)
+{
+ ArrayList data;
+ if (!optionData.GetValue(option, data))
+ {
+ LogError("Failed to get option property of unregistered option \"%s\".", option);
+ return -1;
+ }
+
+ return data.Get(view_as<int>(prop));
+}
+
+bool SetOptionProp(const char[] option, OptionProp prop, any newValue)
+{
+ ArrayList data;
+ if (!optionData.GetValue(option, data))
+ {
+ LogError("Failed to set property of unregistered option \"%s\".", option);
+ return false;
+ }
+
+ if (prop == OptionProp_Cookie)
+ {
+ LogError("Failed to set cookie of option \"%s\" as it is read-only.");
+ return false;
+ }
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ any defaultValue = GetOptionProp(option, OptionProp_DefaultValue);
+ any minValue = GetOptionProp(option, OptionProp_MinValue);
+ any maxValue = GetOptionProp(option, OptionProp_MaxValue);
+
+ switch (prop)
+ {
+ case OptionProp_DefaultValue:
+ {
+ if (!IsValueInRange(type, newValue, minValue, maxValue))
+ {
+ LogError("Failed to set default value of option \"%s\" due to invalid default value and value range.", option);
+ return false;
+ }
+ }
+ case OptionProp_MinValue:
+ {
+ if (!IsValueInRange(type, defaultValue, newValue, maxValue))
+ {
+ LogError("Failed to set minimum value of option \"%s\" due to invalid default value and value range.", option);
+ return false;
+ }
+ }
+ case OptionProp_MaxValue:
+ {
+ if (!IsValueInRange(type, defaultValue, minValue, newValue))
+ {
+ LogError("Failed to set maximum value of option \"%s\" due to invalid default value and value range.", option);
+ return false;
+ }
+ }
+ }
+
+ data.Set(view_as<int>(prop), newValue);
+ return optionData.SetValue(option, data, true);
+}
+
+any GetOption(int client, const char[] option)
+{
+ if (!IsRegisteredOption(option))
+ {
+ LogError("Failed to get value of unregistered option \"%s\".", option);
+ return -1;
+ }
+
+ Cookie cookie = GetOptionProp(option, OptionProp_Cookie);
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ char value[100];
+ cookie.Get(client, value, sizeof(value));
+
+ if (type == OptionType_Float)
+ {
+ return StringToFloat(value);
+ }
+ else //if (type == OptionType_Int)
+ {
+ return StringToInt(value);
+ }
+}
+
+bool SetOption(int client, const char[] option, any newValue)
+{
+ if (!IsRegisteredOption(option))
+ {
+ LogError("Failed to set value of unregistered option \"%s\".", option);
+ return false;
+ }
+
+ if (GetOption(client, option) == newValue)
+ {
+ return true;
+ }
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ any minValue = GetOptionProp(option, OptionProp_MinValue);
+ any maxValue = GetOptionProp(option, OptionProp_MaxValue);
+
+ if (!IsValueInRange(type, newValue, minValue, maxValue))
+ {
+ LogError("Failed to set value of option \"%s\" because desired value was outside registered value range.", option);
+ return false;
+ }
+
+ char newValueString[100];
+ if (type == OptionType_Float)
+ {
+ FloatToString(newValue, newValueString, sizeof(newValueString));
+ }
+ else //if (type == OptionType_Int)
+ {
+ IntToString(newValue, newValueString, sizeof(newValueString));
+ }
+
+ Cookie cookie = GetOptionProp(option, OptionProp_Cookie);
+ cookie.Set(client, newValueString);
+
+ if (IsClientInGame(client))
+ {
+ Call_GOKZ_OnOptionChanged(client, option, newValue);
+ }
+
+ return true;
+}
+
+bool IsRegisteredOption(const char[] option)
+{
+ int dummy;
+ return optionData.GetValue(option, dummy);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_Options()
+{
+ optionData = new StringMap();
+ optionDescriptions = new StringMap();
+ RegisterOptions();
+}
+
+void OnClientCookiesCached_Options(int client)
+{
+ StringMapSnapshot optionDataSnapshot = optionData.Snapshot();
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+
+ for (int i = 0; i < optionDataSnapshot.Length; i++)
+ {
+ optionDataSnapshot.GetKey(i, option, sizeof(option));
+ LoadOption(client, option);
+ }
+
+ delete optionDataSnapshot;
+
+ Call_GOKZ_OnOptionsLoaded(client);
+}
+
+void OnClientPutInServer_Options(int client)
+{
+ if (!GetModeLoaded(GOKZ_GetCoreOption(client, Option_Mode)))
+ {
+ GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode());
+ }
+}
+
+void OnOptionChanged_Options(int client, Option option, int newValue)
+{
+ if (option == Option_Mode && !GetModeLoaded(newValue))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Mode Not Available", newValue);
+ GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode());
+ }
+ else
+ {
+ PrintOptionChangeMessage(client, option, newValue);
+ }
+}
+
+void OnModeUnloaded_Options(int mode)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && GOKZ_GetCoreOption(client, Option_Mode) == mode)
+ {
+ GOKZ_SetCoreOption(client, Option_Mode, GetALoadedMode());
+ }
+ }
+}
+
+void OnMapStart_Options()
+{
+ LoadDefaultOptions();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void RegisterOptions()
+{
+ for (Option option; option < OPTION_COUNT; option++)
+ {
+ RegisterOption(gC_CoreOptionNames[option], gC_CoreOptionDescriptions[option],
+ OptionType_Int, gI_CoreOptionDefaults[option], 0, gI_CoreOptionCounts[option] - 1);
+ }
+}
+
+static bool IsValueInRange(OptionType type, any value, any minValue, any maxValue)
+{
+ if (type == OptionType_Float)
+ {
+ return FloatCompare(minValue, value) <= 0 && FloatCompare(value, maxValue) <= 0;
+ }
+ else //if (type == OptionType_Int)
+ {
+ return minValue <= value && value <= maxValue;
+ }
+}
+
+static void LoadOption(int client, const char[] option)
+{
+ char valueString[100];
+ Cookie cookie = GetOptionProp(option, OptionProp_Cookie);
+ cookie.Get(client, valueString, sizeof(valueString));
+
+ // If there's no stored value for the option, set it to default
+ if (valueString[0] == '\0')
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ return;
+ }
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ any minValue = GetOptionProp(option, OptionProp_MinValue);
+ any maxValue = GetOptionProp(option, OptionProp_MaxValue);
+ any value;
+
+ // If stored option isn't a valid float or integer, or is out of range, set it to default
+ if (type == OptionType_Float && StringToFloatEx(valueString, value) == 0)
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ else if (type == OptionType_Int && StringToIntEx(valueString, value) == 0)
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ else if (!IsValueInRange(type, value, minValue, maxValue))
+ {
+ SetOption(client, option, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+}
+
+// Load default optionData from a config file, creating one and adding optionData if necessary
+static void LoadDefaultOptions()
+{
+ KeyValues oldKV = new KeyValues(GOKZ_CFG_OPTIONS_ROOT);
+
+ if (FileExists(GOKZ_CFG_OPTIONS) && !oldKV.ImportFromFile(GOKZ_CFG_OPTIONS))
+ {
+ LogError("Failed to load file: \"%s\".", GOKZ_CFG_OPTIONS);
+ delete oldKV;
+ return;
+ }
+
+ KeyValues newKV = new KeyValues(GOKZ_CFG_OPTIONS_ROOT); // This one will be sorted by option name
+ StringMapSnapshot optionDataSnapshot = optionData.Snapshot();
+ ArrayList optionDataSnapshotArray = new ArrayList(ByteCountToCells(GOKZ_OPTION_MAX_NAME_LENGTH), 0);
+ char option[GOKZ_OPTION_MAX_NAME_LENGTH];
+ char optionDescription[GOKZ_OPTION_MAX_DESC_LENGTH];
+
+ // Sort the optionData by name
+ for (int i = 0; i < optionDataSnapshot.Length; i++)
+ {
+ optionDataSnapshot.GetKey(i, option, sizeof(option));
+ optionDataSnapshotArray.PushString(option);
+ }
+ SortADTArray(optionDataSnapshotArray, Sort_Ascending, Sort_String);
+
+ // Get the values from the KeyValues, otherwise set them
+ for (int i = 0; i < optionDataSnapshotArray.Length; i++)
+ {
+ oldKV.Rewind();
+ newKV.Rewind();
+ optionDataSnapshotArray.GetString(i, option, sizeof(option));
+ optionDescriptions.GetString(option, optionDescription, sizeof(optionDescription));
+
+ newKV.JumpToKey(option, true);
+ newKV.SetString(GOKZ_CFG_OPTIONS_DESCRIPTION, optionDescription);
+
+ OptionType type = GetOptionProp(option, OptionProp_Type);
+ if (type == OptionType_Float)
+ {
+ if (oldKV.JumpToKey(option, false) && oldKV.JumpToKey(GOKZ_CFG_OPTIONS_DEFAULT, false))
+ {
+ oldKV.GoBack();
+ newKV.SetFloat(GOKZ_CFG_OPTIONS_DEFAULT, oldKV.GetFloat(GOKZ_CFG_OPTIONS_DEFAULT));
+ SetOptionProp(option, OptionProp_DefaultValue, oldKV.GetFloat(GOKZ_CFG_OPTIONS_DEFAULT));
+ }
+ else
+ {
+ newKV.SetFloat(GOKZ_CFG_OPTIONS_DEFAULT, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ }
+ else if (type == OptionType_Int)
+ {
+ if (oldKV.JumpToKey(option, false) && oldKV.JumpToKey(GOKZ_CFG_OPTIONS_DEFAULT, false))
+ {
+ oldKV.GoBack();
+ newKV.SetNum(GOKZ_CFG_OPTIONS_DEFAULT, oldKV.GetNum(GOKZ_CFG_OPTIONS_DEFAULT));
+ SetOptionProp(option, OptionProp_DefaultValue, oldKV.GetNum(GOKZ_CFG_OPTIONS_DEFAULT));
+ }
+ else
+ {
+ newKV.SetNum(GOKZ_CFG_OPTIONS_DEFAULT, GetOptionProp(option, OptionProp_DefaultValue));
+ }
+ }
+ }
+
+ newKV.Rewind();
+ newKV.ExportToFile(GOKZ_CFG_OPTIONS);
+
+ delete oldKV;
+ delete newKV;
+ delete optionDataSnapshot;
+ delete optionDataSnapshotArray;
+}
+
+static void PrintOptionChangeMessage(int client, Option option, int newValue)
+{
+ // NOTE: Not all optionData have a message for when they are changed.
+ switch (option)
+ {
+ case Option_Mode:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Switched Mode", gC_ModeNames[newValue]);
+ }
+ case Option_VirtualButtonIndicators:
+ {
+ switch (newValue)
+ {
+ case VirtualButtonIndicators_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Virtual Button Indicators - Disable");
+ }
+ case VirtualButtonIndicators_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Virtual Button Indicators - Enable");
+ }
+ }
+ }
+ case Option_Safeguard:
+ {
+ switch (newValue)
+ {
+ case Safeguard_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Disable");
+ }
+ case Safeguard_EnabledNUB:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Enable (NUB)");
+ }
+ case Safeguard_EnabledPRO:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Safeguard - Enable (PRO)");
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/teamnumfix.sp b/sourcemod/scripting/gokz-core/teamnumfix.sp
new file mode 100644
index 0000000..0c5d81d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/teamnumfix.sp
@@ -0,0 +1,68 @@
+static Handle H_RemovePlayer;
+static int teamEntID[4];
+static int oldTeam[MAXPLAYERS + 1];
+static int realTeam[MAXPLAYERS + 1];
+
+void OnPluginStart_TeamNumber()
+{
+ GameData gamedataConf = LoadGameConfigFile("gokz-core.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-core gamedata");
+ }
+
+ StartPrepSDKCall(SDKCall_Entity);
+ PrepSDKCall_SetVirtual(gamedataConf.GetOffset("CCSTeam::RemovePlayer"));
+ PrepSDKCall_AddParameter(SDKType_CBasePlayer, SDKPass_Pointer);
+ H_RemovePlayer = EndPrepSDKCall();
+ if (H_RemovePlayer == INVALID_HANDLE)
+ {
+ SetFailState("Unable to prepare SDKCall for CCSTeam::RemovePlayer!");
+ }
+}
+
+void OnMapStart_TeamNumber()
+{
+ // Fetch the entity ID of team entities and store them.
+ int team = FindEntityByClassname(MaxClients + 1, "cs_team_manager");
+ while (team != -1)
+ {
+ int teamNum = GetEntProp(team, Prop_Send, "m_iTeamNum");
+ teamEntID[teamNum] = team;
+ team = FindEntityByClassname(team, "cs_team_manager");
+ }
+}
+
+void OnGameFrame_TeamNumber()
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (!IsClientInGame(client) || !IsPlayerAlive(client))
+ {
+ continue;
+ }
+ int team = GetEntProp(client, Prop_Data, "m_iTeamNum");
+ // If the entprop changed, remove the player from the old team, but make sure it's a valid team first
+ if (team != oldTeam[client] && oldTeam[client] < 4 && oldTeam[client] > 0)
+ {
+ SDKCall(H_RemovePlayer, teamEntID[oldTeam[client]], client);
+ }
+ oldTeam[client] = team;
+ }
+}
+
+void OnPlayerJoinTeam_TeamNumber(Event event, int client)
+{
+ // If the old team value is invalid, fix it.
+ if (event.GetInt("oldteam") > 4 || event.GetInt("oldteam") < 0)
+ {
+ event.SetInt("oldteam", 0);
+ }
+ realTeam[client] = event.GetInt("team");
+}
+
+void OnPlayerDeath_TeamNumber(int client)
+{
+ // Switch the client's team to a valid team to prevent crashes.
+ CS_SwitchTeam(client, realTeam[client]);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/teleports.sp b/sourcemod/scripting/gokz-core/teleports.sp
new file mode 100644
index 0000000..764fc6e
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/teleports.sp
@@ -0,0 +1,917 @@
+/*
+ Checkpoints and teleporting, including ability to go back
+ to previous checkpoint, go to next checkpoint, and undo.
+*/
+
+
+static ArrayList checkpoints[MAXPLAYERS + 1];
+static int checkpointCount[MAXPLAYERS + 1];
+static int checkpointIndex[MAXPLAYERS + 1];
+static int checkpointIndexStart[MAXPLAYERS + 1];
+static int checkpointIndexEnd[MAXPLAYERS + 1];
+static int teleportCount[MAXPLAYERS + 1];
+static StartPositionType startType[MAXPLAYERS + 1];
+static StartPositionType nonCustomStartType[MAXPLAYERS + 1];
+static float nonCustomStartOrigin[MAXPLAYERS + 1][3];
+static float nonCustomStartAngles[MAXPLAYERS + 1][3];
+static float customStartOrigin[MAXPLAYERS + 1][3];
+static float customStartAngles[MAXPLAYERS + 1][3];
+static float endOrigin[MAXPLAYERS + 1][3];
+static float endAngles[MAXPLAYERS + 1][3];
+static UndoTeleportData undoTeleportData[MAXPLAYERS + 1];
+static float lastRestartAttemptTime[MAXPLAYERS + 1];
+
+// =====[ PUBLIC ]=====
+
+int GetCheckpointCount(int client)
+{
+ return checkpointCount[client];
+}
+
+void SetCheckpointCount(int client, int cpCount)
+{
+ checkpointCount[client] = cpCount;
+}
+
+int GetTeleportCount(int client)
+{
+ return teleportCount[client];
+}
+
+void SetTeleportCount(int client, int tpCount)
+{
+ teleportCount[client] = tpCount;
+}
+
+// CHECKPOINT
+
+void OnMapStart_Checkpoints()
+{
+ for (int client = 0; client < MAXPLAYERS + 1; client++)
+ {
+ if (checkpoints[client] != INVALID_HANDLE)
+ {
+ delete checkpoints[client];
+ }
+ checkpoints[client] = new ArrayList(sizeof(Checkpoint));
+ }
+}
+
+void MakeCheckpoint(int client)
+{
+ if (!CanMakeCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnMakeCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ // Make Checkpoint
+ checkpointCount[client]++;
+ Checkpoint cp;
+ cp.Create(client);
+
+ if (checkpoints[client] == INVALID_HANDLE)
+ {
+ checkpoints[client] = new ArrayList(sizeof(Checkpoint));
+ }
+
+ checkpointIndex[client] = NextIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS);
+ checkpointIndexEnd[client] = checkpointIndex[client];
+ // The list has yet to be filled up, do PushArray instead of SetArray
+ if (checkpoints[client].Length < GOKZ_MAX_CHECKPOINTS && checkpointIndex[client] == checkpoints[client].Length)
+ {
+ checkpoints[client].PushArray(cp);
+ // Initialize start and end index for the first checkpoint
+ if (checkpoints[client].Length == 1)
+ {
+ checkpointIndexStart[client] = 0;
+ checkpointIndexEnd[client] = 0;
+ }
+ }
+ else
+ {
+ checkpoints[client].SetArray(checkpointIndex[client], cp);
+ // The new checkpoint has overridden the oldest checkpoint, move the start index by one.
+ if (checkpointIndexEnd[client] == checkpointIndexStart[client])
+ {
+ checkpointIndexStart[client] = NextIndex(checkpointIndexStart[client], GOKZ_MAX_CHECKPOINTS);
+ }
+ }
+
+
+ if (GOKZ_GetCoreOption(client, Option_CheckpointSounds) == CheckpointSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_CHECKPOINT, _, "Checkpoint");
+ }
+ if (GOKZ_GetCoreOption(client, Option_CheckpointMessages) == CheckpointMessages_Enabled)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Make Checkpoint", checkpointCount[client]);
+ }
+
+ if (!GetTimerRunning(client) && AntiCpTriggerIsTouched(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Anti Checkpoint Area Warning");
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnMakeCheckpoint_Post(client);
+}
+
+bool CanMakeCheckpoint(int client, bool showError = false)
+{
+ if (!IsPlayerAlive(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Must Be Alive");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (GetTimerRunning(client) && AntiCpTriggerIsTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Anti Checkpoint Area)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (!Movement_GetOnGround(client) && Movement_GetMovetype(client) != MOVETYPE_LADDER)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (BhopTriggersJustTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Checkpoint (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+ArrayList GetCheckpointData(int client)
+{
+ // Don't clone the entire thing, return an ordered list of checkpoints.
+ // Doing this should be cleaner, saves memory and should be faster than a full Clone().
+ ArrayList checkpointData = new ArrayList(sizeof(Checkpoint));
+ if (checkpointIndex[client] == -1)
+ {
+ // No checkpoint was made, return empty ArrayList
+ return checkpointData;
+ }
+ for (int i = checkpointIndexStart[client]; i != checkpointIndexEnd[client]; i = NextIndex(i, GOKZ_MAX_CHECKPOINTS))
+ {
+ Checkpoint cp;
+ checkpoints[client].GetArray(i, cp);
+ checkpointData.PushArray(cp);
+ }
+ return checkpointData;
+}
+
+bool SetCheckpointData(int client, ArrayList cps, int version)
+{
+ if (version != GOKZ_CHECKPOINT_VERSION)
+ {
+ return false;
+ }
+ // cps is assumed to be ordered.
+ if (cps != INVALID_HANDLE)
+ {
+ delete checkpoints[client];
+ checkpoints[client] = cps.Clone();
+ if (cps.Length == 0)
+ {
+ checkpointIndexStart[client] = -1;
+ checkpointIndexEnd[client] = -1;
+ }
+ else
+ {
+ checkpointIndexStart[client] = 0;
+ checkpointIndexEnd[client] = checkpoints[client].Length - 1;
+ }
+ checkpointIndex[client] = checkpointIndexEnd[client];
+ return true;
+ }
+ return false;
+}
+
+ArrayList GetUndoTeleportData(int client)
+{
+ // Enum structs cannot be sent directly over natives, we put it in an ArrayList of one instead.
+ // We use another struct instead of reusing Checkpoint so normal checkpoints don't use more memory than needed.
+ ArrayList undoTeleportDataArray = new ArrayList(sizeof(UndoTeleportData));
+ undoTeleportDataArray.PushArray(undoTeleportData[client]);
+ return undoTeleportDataArray;
+}
+
+bool SetUndoTeleportData(int client, ArrayList undoTeleportDataArray, int version)
+{
+ if (version != GOKZ_CHECKPOINT_VERSION)
+ {
+ return false;
+ }
+ if (undoTeleportDataArray != INVALID_HANDLE && undoTeleportDataArray.Length == 1)
+ {
+ undoTeleportDataArray.GetArray(0, undoTeleportData[client], sizeof(UndoTeleportData));
+ return true;
+ }
+ return false;
+}
+// TELEPORT
+
+void TeleportToCheckpoint(int client)
+{
+ if (!CanTeleportToCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ CheckpointTeleportDo(client);
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToCheckpoint_Post(client);
+}
+
+bool CanTeleportToCheckpoint(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client) && GOKZ_GetTeleportCount(client) == 0)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (checkpoints[client] == INVALID_HANDLE || checkpoints[client].Length <= 0)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (No Checkpoints)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+// PREV CP
+
+void PrevCheckpoint(int client)
+{
+ if (!CanPrevCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnPrevCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ checkpointIndex[client] = PrevIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS);
+ CheckpointTeleportDo(client);
+
+ // Call Post Forward
+ Call_GOKZ_OnPrevCheckpoint_Post(client);
+}
+
+bool CanPrevCheckpoint(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) == Safeguard_EnabledPRO && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (checkpointIndex[client] == checkpointIndexStart[client])
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Prev CP (No Checkpoints)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+// NEXT CP
+
+void NextCheckpoint(int client)
+{
+ if (!CanNextCheckpoint(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnNextCheckpoint(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+ checkpointIndex[client] = NextIndex(checkpointIndex[client], GOKZ_MAX_CHECKPOINTS);
+ CheckpointTeleportDo(client);
+
+ // Call Post Forward
+ Call_GOKZ_OnNextCheckpoint_Post(client);
+}
+
+bool CanNextCheckpoint(int client, bool showError = false)
+{
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro && GetTimerRunning(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Teleport (Map)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (checkpointIndex[client] == checkpointIndexEnd[client])
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Next CP (No Checkpoints)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+// RESTART & RESPAWN
+
+bool CanTeleportToStart(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ float currentTime = GetEngineTime();
+ float timeSinceLastAttempt = currentTime - lastRestartAttemptTime[client];
+ float cooldown;
+ // If the client restarts for the first time or the last attempt is too long ago, restart the cooldown.
+ if (lastRestartAttemptTime[client] == 0.0 || timeSinceLastAttempt > GOKZ_SAFEGUARD_RESTART_MAX_DELAY)
+ {
+ lastRestartAttemptTime[client] = currentTime;
+ cooldown = GOKZ_SAFEGUARD_RESTART_MIN_DELAY;
+ }
+ else
+ {
+ cooldown = GOKZ_SAFEGUARD_RESTART_MIN_DELAY - timeSinceLastAttempt;
+ }
+ if (cooldown <= 0.0)
+ {
+ lastRestartAttemptTime[client] = 0.0;
+ return true;
+ }
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked (Temp)", cooldown);
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TeleportToStart(int client)
+{
+ if (!CanTeleportToStart(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToStart(client, GetCurrentCourse(client), result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ // Teleport to Start
+ if (startType[client] == StartPositionType_Spawn)
+ {
+ GOKZ_RespawnPlayer(client, .restorePos = false);
+ // Respawning alone does not guarantee a valid spawn.
+ float spawnOrigin[3];
+ float spawnAngles[3];
+ GetValidSpawn(spawnOrigin, spawnAngles);
+ TeleportPlayer(client, spawnOrigin, spawnAngles);
+ }
+ else if (startType[client] == StartPositionType_Custom)
+ {
+ TeleportDo(client, customStartOrigin[client], customStartAngles[client]);
+ }
+ else
+ {
+ TeleportDo(client, nonCustomStartOrigin[client], nonCustomStartAngles[client]);
+ }
+
+ if (startType[client] != StartPositionType_MapButton
+ && (!InRangeOfVirtualStart(client) || !CanReachVirtualStart(client)))
+ {
+ GOKZ_StopTimer(client, false);
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToStart_Post(client, GetCurrentCourse(client));
+}
+
+void TeleportToSearchStart(int client, int course)
+{
+ if (!CanTeleportToStart(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ float origin[3], angles[3];
+ if (!GetSearchStartPosition(course, origin, angles))
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found (Bonus)", course);
+ }
+ return;
+ }
+ GOKZ_StopTimer(client, false);
+
+ TeleportDo(client, origin, angles);
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToStart_Post(client, course);
+}
+
+StartPositionType GetStartPosition(int client, float position[3], float angles[3])
+{
+ if (startType[client] == StartPositionType_Custom)
+ {
+ position = customStartOrigin[client];
+ angles = customStartAngles[client];
+ }
+ else if (startType[client] != StartPositionType_Spawn)
+ {
+ position = nonCustomStartOrigin[client];
+ angles = nonCustomStartAngles[client];
+ }
+
+ return startType[client];
+}
+
+bool TeleportToCourseStart(int client, int course)
+{
+ if (!CanTeleportToStart(client, true))
+ {
+ return false;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+ float origin[3], angles[3];
+
+ if (!GetMapStartPosition(course, origin, angles))
+ {
+ if (!GetSearchStartPosition(course, origin, angles))
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found (Bonus)", course);
+ }
+ return false;
+ }
+ }
+
+ GOKZ_StopTimer(client);
+
+ TeleportDo(client, origin, angles);
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToStart_Post(client, course);
+ return true;
+}
+
+StartPositionType GetStartPositionType(int client)
+{
+ return startType[client];
+}
+
+// Note: Use ClearStartPosition to switch off StartPositionType_Custom
+void SetStartPosition(int client, StartPositionType type, const float origin[3] = NULL_VECTOR, const float angles[3] = NULL_VECTOR)
+{
+ if (type == StartPositionType_Custom)
+ {
+ startType[client] = StartPositionType_Custom;
+
+ if (!IsNullVector(origin))
+ {
+ customStartOrigin[client] = origin;
+ }
+
+ if (!IsNullVector(angles))
+ {
+ customStartAngles[client] = angles;
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnStartPositionSet_Post(client, startType[client], customStartOrigin[client], customStartAngles[client]);
+ }
+ else
+ {
+ nonCustomStartType[client] = type;
+
+ if (!IsNullVector(origin))
+ {
+ nonCustomStartOrigin[client] = origin;
+ }
+
+ if (!IsNullVector(angles))
+ {
+ nonCustomStartAngles[client] = angles;
+ }
+
+ if (startType[client] != StartPositionType_Custom)
+ {
+ startType[client] = type;
+
+ // Call Post Forward
+ Call_GOKZ_OnStartPositionSet_Post(client, startType[client], nonCustomStartOrigin[client], nonCustomStartAngles[client]);
+ }
+ }
+}
+
+void SetStartPositionToCurrent(int client, StartPositionType type)
+{
+ float origin[3], angles[3];
+ Movement_GetOrigin(client, origin);
+ Movement_GetEyeAngles(client, angles);
+
+ SetStartPosition(client, type, origin, angles);
+}
+
+bool SetStartPositionToMapStart(int client, int course)
+{
+ float origin[3], angles[3];
+
+ if (!GetMapStartPosition(course, origin, angles))
+ {
+ return false;
+ }
+
+ SetStartPosition(client, StartPositionType_MapStart, origin, angles);
+
+ return true;
+}
+
+bool ClearCustomStartPosition(int client)
+{
+ if (GetStartPositionType(client) != StartPositionType_Custom)
+ {
+ return false;
+ }
+
+ startType[client] = nonCustomStartType[client];
+
+ // Call Post Forward
+ Call_GOKZ_OnStartPositionSet_Post(client, startType[client], nonCustomStartOrigin[client], nonCustomStartAngles[client]);
+
+ return true;
+}
+
+
+// TELEPORT TO END
+
+bool CanTeleportToEnd(int client, bool showError = false)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TeleportToEnd(int client, int course)
+{
+ if (!CanTeleportToEnd(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTeleportToEnd(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ GOKZ_StopTimer(client, false);
+
+ if (!GetMapEndPosition(course, endOrigin[client], endAngles[client]))
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No End Found");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No End Found (Bonus)", course);
+ }
+ return;
+ }
+ TeleportDo(client, endOrigin[client], endAngles[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnTeleportToEnd_Post(client, course);
+}
+
+void SetEndPosition(int client, const float origin[3] = NULL_VECTOR, const float angles[3] = NULL_VECTOR)
+{
+ if (!IsNullVector(origin))
+ {
+ endOrigin[client] = origin;
+ }
+ if (!IsNullVector(angles))
+ {
+ endAngles[client] = angles;
+ }
+}
+
+bool SetEndPositionToMapEnd(int client, int course)
+{
+ float origin[3], angles[3];
+
+ if (!GetMapEndPosition(course, origin, angles))
+ {
+ return false;
+ }
+
+ SetEndPosition(client, origin, angles);
+
+ return true;
+}
+
+
+// UNDO TP
+
+void UndoTeleport(int client)
+{
+ if (!CanUndoTeleport(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnUndoTeleport(client, result);
+ if (result != Plugin_Continue)
+ {
+ return;
+ }
+
+ // Undo Teleport
+ TeleportDo(client, undoTeleportData[client].origin, undoTeleportData[client].angles);
+
+ // Call Post Forward
+ Call_GOKZ_OnUndoTeleport_Post(client);
+}
+
+bool CanUndoTeleport(int client, bool showError = false)
+{
+ if (teleportCount[client] <= 0)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (No Teleports)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (!undoTeleportData[client].lastTeleportOnGround)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (TP Was Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (undoTeleportData[client].lastTeleportInBhopTrigger)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (undoTeleportData[client].lastTeleportInAntiCpTrigger)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Undo (AntiCp)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Teleports(int client)
+{
+ checkpointCount[client] = 0;
+ checkpointIndex[client] = -1;
+ checkpointIndexStart[client] = -1;
+ checkpointIndexEnd[client] = -1;
+ teleportCount[client] = 0;
+ startType[client] = StartPositionType_Spawn;
+ nonCustomStartType[client] = StartPositionType_Spawn;
+ lastRestartAttemptTime[client] = 0.0;
+ if (checkpoints[client] != INVALID_HANDLE)
+ {
+ checkpoints[client].Clear();
+ }
+ // Set start and end position to main course if we know of it
+ SetStartPositionToMapStart(client, 0);
+ SetEndPositionToMapEnd(client, 0);
+
+}
+
+void OnTimerStart_Teleports(int client)
+{
+ checkpointCount[client] = 0;
+ checkpointIndex[client] = -1;
+ checkpointIndexStart[client] = -1;
+ checkpointIndexEnd[client] = -1;
+ teleportCount[client] = 0;
+ checkpoints[client].Clear();
+}
+
+void OnStartButtonPress_Teleports(int client, int course)
+{
+ SetStartPositionToCurrent(client, StartPositionType_MapButton);
+ SetEndPositionToMapEnd(client, course);
+}
+
+void OnVirtualStartButtonPress_Teleports(int client)
+{
+ SetStartPositionToCurrent(client, StartPositionType_MapButton);
+}
+
+void OnStartZoneStartTouch_Teleports(int client, int course)
+{
+ SetStartPositionToMapStart(client, course);
+ SetEndPositionToMapEnd(client, course);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static int PrevIndex(int current, int maximum)
+{
+ int prev = current - 1;
+ if (prev < 0)
+ {
+ return maximum - 1;
+ }
+ return prev;
+}
+
+static void TeleportDo(int client, const float destOrigin[3], const float destAngles[3])
+{
+ if (!IsPlayerAlive(client))
+ {
+ GOKZ_RespawnPlayer(client);
+ }
+
+ // Store information about where player is teleporting from
+ undoTeleportData[client].Init(client, BhopTriggersJustTouched(client), Movement_GetOnGround(client), AntiCpTriggerIsTouched(client));
+
+ teleportCount[client]++;
+ TeleportPlayer(client, destOrigin, destAngles);
+ // TeleportPlayer needs to be done before undo TP data can be fully updated.
+ undoTeleportData[client].Update();
+ if (GOKZ_GetCoreOption(client, Option_TeleportSounds) == TeleportSounds_Enabled)
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TELEPORT, _, "Teleport");
+ }
+
+ // Call Post Foward
+ Call_GOKZ_OnCountedTeleport_Post(client);
+}
+
+static void CheckpointTeleportDo(int client)
+{
+ Checkpoint cp;
+ checkpoints[client].GetArray(checkpointIndex[client], cp);
+
+ TeleportDo(client, cp.origin, cp.angles);
+ if (cp.groundEnt != INVALID_ENT_REFERENCE)
+ {
+ SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", cp.groundEnt);
+ SetEntityFlags(client, GetEntityFlags(client) | FL_ONGROUND);
+ }
+ // Handle ladder stuff
+ if (cp.onLadder)
+ {
+ SetEntPropVector(client, Prop_Send, "m_vecLadderNormal", cp.ladderNormal);
+ if (!GOKZ_GetPaused(client))
+ {
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+ else
+ {
+ SetPausedOnLadder(client, true);
+ }
+ }
+ else if (GOKZ_GetPaused(client))
+ {
+ SetPausedOnLadder(client, false);
+ }
+}
diff --git a/sourcemod/scripting/gokz-core/timer/pause.sp b/sourcemod/scripting/gokz-core/timer/pause.sp
new file mode 100644
index 0000000..92ab1fb
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/pause.sp
@@ -0,0 +1,257 @@
+static bool paused[MAXPLAYERS + 1];
+static bool pausedOnLadder[MAXPLAYERS + 1];
+static float lastPauseTime[MAXPLAYERS + 1];
+static bool hasPausedInThisRun[MAXPLAYERS + 1];
+static float lastResumeTime[MAXPLAYERS + 1];
+static bool hasResumedInThisRun[MAXPLAYERS + 1];
+static float lastDuckValue[MAXPLAYERS + 1];
+static float lastStaminaValue[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetPaused(int client)
+{
+ return paused[client];
+}
+
+void SetPausedOnLadder(int client, bool onLadder)
+{
+ pausedOnLadder[client] = onLadder;
+}
+
+void Pause(int client)
+{
+ if (!CanPause(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnPause(client, result);
+ if (result != Plugin_Continue)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Generic)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ // Pause
+ paused[client] = true;
+ pausedOnLadder[client] = Movement_GetMovetype(client) == MOVETYPE_LADDER;
+ lastDuckValue[client] = Movement_GetDuckSpeed(client);
+ lastStaminaValue[client] = GetEntPropFloat(client, Prop_Send, "m_flStamina");
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ Movement_SetMovetype(client, MOVETYPE_NONE);
+ if (GetTimerRunning(client))
+ {
+ hasPausedInThisRun[client] = true;
+ lastPauseTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnPause_Post(client);
+}
+
+bool CanPause(int client, bool showError = false)
+{
+ if (paused[client])
+ {
+ return false;
+ }
+
+ if (GetTimerRunning(client))
+ {
+ if (hasResumedInThisRun[client]
+ && GetEngineTime() - lastResumeTime[client] < GOKZ_PAUSE_COOLDOWN)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Resumed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (!Movement_GetOnGround(client)
+ && !(Movement_GetSpeed(client) == 0 && Movement_GetVerticalVelocity(client) == 0))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (BhopTriggersJustTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (AntiPauseTriggerIsTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Anti Pause Area)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Resume(int client, bool force = false)
+{
+ if (!paused[client])
+ {
+ return;
+ }
+ if (!force && !CanResume(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnResume(client, result);
+ if (result != Plugin_Continue)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Generic)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ // Resume
+ if (pausedOnLadder[client])
+ {
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+ else
+ {
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+ Movement_SetDuckSpeed(client, lastDuckValue[client]);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+bool CanResume(int client, bool showError = false)
+{
+ if (GetTimerRunning(client) && hasPausedInThisRun[client]
+ && GetEngineTime() - lastPauseTime[client] < GOKZ_PAUSE_COOLDOWN)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Just Paused)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TogglePause(int client)
+{
+ if (paused[client])
+ {
+ Resume(client);
+ }
+ else
+ {
+ Pause(client);
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Pause(int client)
+{
+ paused[client] = false;
+}
+
+void OnTimerStart_Pause(int client)
+{
+ hasPausedInThisRun[client] = false;
+ hasResumedInThisRun[client] = false;
+ Resume(client, true);
+}
+
+void OnChangeMovetype_Pause(int client, MoveType newMovetype)
+{
+ // Check if player has escaped MOVETYPE_NONE
+ if (!paused[client] || newMovetype == MOVETYPE_NONE)
+ {
+ return;
+ }
+
+ // Player has escaped MOVETYPE_NONE, so resume
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+void OnPlayerSpawn_Pause(int client)
+{
+ if (!paused[client])
+ {
+ return;
+ }
+
+ // Player has left paused state by spawning in, so resume
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+
+ Movement_SetDuckSpeed(client, lastDuckValue[client]);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+void OnJoinTeam_Pause(int client, int team)
+{
+ // Only handle joining spectators. Joining other teams is handled by OnPlayerSpawn.
+ if (team == CS_TEAM_SPECTATOR)
+ {
+ paused[client] = true;
+
+ if (GetTimerRunning(client))
+ {
+ hasPausedInThisRun[client] = true;
+ lastPauseTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnPause_Post(client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/timer/timer.sp b/sourcemod/scripting/gokz-core/timer/timer.sp
new file mode 100644
index 0000000..f6696ac
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/timer.sp
@@ -0,0 +1,368 @@
+static bool timerRunning[MAXPLAYERS + 1];
+static float currentTime[MAXPLAYERS + 1];
+static int currentCourse[MAXPLAYERS + 1];
+static float lastEndTime[MAXPLAYERS + 1];
+static float lastFalseEndTime[MAXPLAYERS + 1];
+static float lastStartSoundTime[MAXPLAYERS + 1];
+static int lastStartMode[MAXPLAYERS + 1];
+static bool validTime[MAXPLAYERS + 1];
+
+
+// =====[ PUBLIC ]=====
+
+bool GetTimerRunning(int client)
+{
+ return timerRunning[client];
+}
+
+bool GetValidTimer(int client)
+{
+ return validTime[client];
+}
+
+float GetCurrentTime(int client)
+{
+ return currentTime[client];
+}
+
+void SetCurrentTime(int client, float time)
+{
+ currentTime[client] = time;
+ // The timer should be running if time is not negative.
+ timerRunning[client] = time >= 0.0;
+}
+
+int GetCurrentCourse(int client)
+{
+ return currentCourse[client];
+}
+
+void SetCurrentCourse(int client, int course)
+{
+ currentCourse[client] = course;
+}
+
+int GetCurrentTimeType(int client)
+{
+ if (GetTeleportCount(client) == 0)
+ {
+ return TimeType_Pro;
+ }
+ return TimeType_Nub;
+}
+
+bool TimerStart(int client, int course, bool allowMidair = false, bool playSound = true)
+{
+ if (!IsPlayerAlive(client)
+ || JustStartedTimer(client)
+ || JustTeleported(client)
+ || JustNoclipped(client)
+ || !IsPlayerValidMoveType(client)
+ || !allowMidair && (!Movement_GetOnGround(client) || JustLanded(client))
+ || allowMidair && !Movement_GetOnGround(client) && (!GOKZ_GetValidJump(client) || GOKZ_GetHitPerf(client))
+ || (GOKZ_GetTimerRunning(client) && GOKZ_GetCourse(client) != course))
+ {
+ return false;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTimerStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Start Timer
+ currentTime[client] = 0.0;
+ timerRunning[client] = true;
+ currentCourse[client] = course;
+ lastStartMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ validTime[client] = true;
+ if (playSound)
+ {
+ PlayTimerStartSound(client);
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTimerStart_Post(client, course);
+
+ return true;
+}
+
+bool TimerEnd(int client, int course)
+{
+ if (!IsPlayerAlive(client))
+ {
+ return false;
+ }
+
+ if (!timerRunning[client] || course != currentCourse[client])
+ {
+ PlayTimerFalseEndSound(client);
+ lastFalseEndTime[client] = GetGameTime();
+ return false;
+ }
+
+ float time = GetCurrentTime(client);
+ int teleportsUsed = GetTeleportCount(client);
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTimerEnd(client, course, time, teleportsUsed, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+
+ if (!validTime[client])
+ {
+ PlayTimerFalseEndSound(client);
+ lastFalseEndTime[client] = GetGameTime();
+ TimerStop(client, false);
+ return false;
+ }
+ // End Timer
+ timerRunning[client] = false;
+ lastEndTime[client] = GetGameTime();
+ PlayTimerEndSound(client);
+
+ if (!IsFakeClient(client))
+ {
+ // Print end timer message
+ Call_GOKZ_OnTimerEndMessage(client, course, time, teleportsUsed, result);
+ if (result == Plugin_Continue)
+ {
+ PrintEndTimeString(client);
+ }
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTimerEnd_Post(client, course, time, teleportsUsed);
+
+ return true;
+}
+
+bool TimerStop(int client, bool playSound = true)
+{
+ if (!timerRunning[client])
+ {
+ return false;
+ }
+
+ timerRunning[client] = false;
+ if (playSound)
+ {
+ PlayTimerStopSound(client);
+ }
+
+ Call_GOKZ_OnTimerStopped(client);
+
+ return true;
+}
+
+void TimerStopAll(bool playSound = true)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client))
+ {
+ TimerStop(client, playSound);
+ }
+ }
+}
+
+void PlayTimerStartSound(int client)
+{
+ if (GetGameTime() - lastStartSoundTime[client] > GOKZ_TIMER_SOUND_COOLDOWN)
+ {
+ GOKZ_EmitSoundToClient(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ lastStartSoundTime[client] = GetGameTime();
+ }
+}
+
+void InvalidateRun(int client)
+{
+ if (validTime[client])
+ {
+ validTime[client] = false;
+ Call_GOKZ_OnRunInvalidated(client);
+ }
+}
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Timer(int client)
+{
+ timerRunning[client] = false;
+ currentTime[client] = 0.0;
+ currentCourse[client] = 0;
+ lastEndTime[client] = 0.0;
+ lastFalseEndTime[client] = 0.0;
+ lastStartSoundTime[client] = 0.0;
+ lastStartMode[client] = MODE_COUNT; // So it won't equal any mode
+}
+
+void OnPlayerRunCmdPost_Timer(int client)
+{
+ if (IsPlayerAlive(client) && GetTimerRunning(client) && !GetPaused(client))
+ {
+ currentTime[client] += GetTickInterval();
+ }
+}
+
+void OnChangeMovetype_Timer(int client, MoveType newMovetype)
+{
+ if (!IsValidMovetype(newMovetype))
+ {
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Noclipped)");
+ }
+ }
+}
+
+void OnTeleportToStart_Timer(int client)
+{
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro)
+ {
+ TimerStop(client, false);
+ }
+}
+
+void OnClientDisconnect_Timer(int client)
+{
+ TimerStop(client);
+}
+
+void OnPlayerDeath_Timer(int client)
+{
+ TimerStop(client);
+}
+
+void OnOptionChanged_Timer(int client, Option option)
+{
+ if (option == Option_Mode)
+ {
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Changed Mode)");
+ }
+ }
+}
+
+void OnRoundStart_Timer()
+{
+ TimerStopAll();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static bool IsPlayerValidMoveType(int client)
+{
+ return IsValidMovetype(Movement_GetMovetype(client));
+}
+
+static bool IsValidMovetype(MoveType movetype)
+{
+ return movetype == MOVETYPE_WALK
+ || movetype == MOVETYPE_LADDER
+ || movetype == MOVETYPE_NONE
+ || movetype == MOVETYPE_OBSERVER;
+}
+
+static bool JustTeleported(int client)
+{
+ return gB_OriginTeleported[client] || gB_VelocityTeleported[client]
+ || gI_CmdNum[client] - gI_TeleportCmdNum[client] <= GOKZ_TIMER_START_GROUND_TICKS;
+}
+
+static bool JustLanded(int client)
+{
+ return !gB_OldOnGround[client]
+ || gI_CmdNum[client] - Movement_GetLandingCmdNum(client) <= GOKZ_TIMER_START_NO_TELEPORT_TICKS;
+}
+
+static bool JustStartedTimer(int client)
+{
+ return timerRunning[client] && GetCurrentTime(client) < EPSILON;
+}
+
+static bool JustEndedTimer(int client)
+{
+ return GetGameTime() - lastEndTime[client] < 1.0;
+}
+
+static void PlayTimerEndSound(int client)
+{
+ GOKZ_EmitSoundToClient(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+}
+
+static void PlayTimerFalseEndSound(int client)
+{
+ if (!JustEndedTimer(client)
+ && (GetGameTime() - lastFalseEndTime[client]) > GOKZ_TIMER_SOUND_COOLDOWN)
+ {
+ GOKZ_EmitSoundToClient(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End");
+ }
+}
+
+static void PlayTimerStopSound(int client)
+{
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+ GOKZ_EmitSoundToClientSpectators(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+}
+
+static void PrintEndTimeString(int client)
+{
+ if (GetCurrentCourse(client) == 0)
+ {
+ switch (GetCurrentTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Map (NUB)",
+ client,
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Map (PRO)",
+ client,
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+ else
+ {
+ switch (GetCurrentTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (NUB)",
+ client,
+ currentCourse[client],
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (PRO)",
+ client,
+ currentCourse[client],
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp
new file mode 100644
index 0000000..aa88a9d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp
@@ -0,0 +1,322 @@
+/*
+ Most commonly referred to in the KZ community as timer tech.
+ Lets players press 'virtual' start and end buttons without looking.
+*/
+
+
+
+static int beamSprite;
+static int haloSprite;
+static float lastUsePressTime[MAXPLAYERS + 1];
+static int lastTeleportTick[MAXPLAYERS + 1];
+static bool startedTimerLastTick[MAXPLAYERS + 1];
+static bool onlyNaturalButtonPressed[MAXPLAYERS + 1];
+static int startTimerButtonPressTick[MAXPLAYERS + 1];
+static bool hasEndedTimerSincePressingUse[MAXPLAYERS + 1];
+static bool hasTeleportedSincePressingUse[MAXPLAYERS + 1];
+static bool hasVirtualStartButton[MAXPLAYERS + 1];
+static bool hasVirtualEndButton[MAXPLAYERS + 1];
+static bool wasInEndZone[MAXPLAYERS + 1];
+static float virtualStartOrigin[MAXPLAYERS + 1][3];
+static float virtualEndOrigin[MAXPLAYERS + 1][3];
+static int virtualStartCourse[MAXPLAYERS + 1];
+static int virtualEndCourse[MAXPLAYERS + 1];
+static bool virtualButtonsLocked[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetHasVirtualStartButton(int client)
+{
+ return hasVirtualStartButton[client];
+}
+
+bool GetHasVirtualEndButton(int client)
+{
+ return hasVirtualEndButton[client];
+}
+
+bool ToggleVirtualButtonsLock(int client)
+{
+ virtualButtonsLocked[client] = !virtualButtonsLocked[client];
+ return virtualButtonsLocked[client];
+}
+
+void LockVirtualButtons(int client)
+{
+ virtualButtonsLocked[client] = true;
+}
+
+int GetVirtualButtonPosition(int client, float position[3], bool isStart)
+{
+ if (isStart && hasVirtualStartButton[client])
+ {
+ position = virtualStartOrigin[client];
+ return virtualStartCourse[client];
+ }
+ else if (!isStart && hasVirtualEndButton[client])
+ {
+ position = virtualEndOrigin[client];
+ return virtualEndCourse[client];
+ }
+
+ return -1;
+}
+
+void SetVirtualButtonPosition(int client, float position[3], int course, bool isStart)
+{
+ if (isStart)
+ {
+ virtualStartCourse[client] = course;
+ virtualStartOrigin[client] = position;
+ hasVirtualStartButton[client] = true;
+ }
+ else
+ {
+ virtualEndCourse[client] = course;
+ virtualEndOrigin[client] = position;
+ hasVirtualEndButton[client] = true;
+ }
+}
+
+void ResetVirtualButtonPosition(int client, bool isStart)
+{
+ if (isStart)
+ {
+ virtualStartCourse[client] = -1;
+ virtualStartOrigin[client] = {0.0, 0.0, 0.0};
+ hasVirtualStartButton[client] = false;
+ }
+ else
+ {
+ virtualEndCourse[client] = -1;
+ virtualEndOrigin[client] = {0.0, 0.0, 0.0};
+ hasVirtualEndButton[client] = false;
+ }
+}
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_VirtualButtons()
+{
+ beamSprite = PrecacheModel("materials/sprites/laserbeam.vmt");
+ haloSprite = PrecacheModel("materials/sprites/glow01.vmt");
+}
+
+void OnClientPutInServer_VirtualButtons(int client)
+{
+ startedTimerLastTick[client] = false;
+ hasVirtualEndButton[client] = false;
+ hasVirtualStartButton[client] = false;
+ virtualButtonsLocked[client] = false;
+ onlyNaturalButtonPressed[client] = false;
+ wasInEndZone[client] = false;
+ startTimerButtonPressTick[client] = 0;
+}
+
+void OnStartButtonPress_VirtualButtons(int client, int course)
+{
+ if (!virtualButtonsLocked[client] &&
+ lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount())
+ {
+ Movement_GetOrigin(client, virtualStartOrigin[client]);
+ virtualStartCourse[client] = course;
+ hasVirtualStartButton[client] = true;
+ startTimerButtonPressTick[client] = GetGameTickCount();
+ }
+}
+
+void OnEndButtonPress_VirtualButtons(int client, int course)
+{
+ // Prevent setting end virtual button to where it would usually be unreachable
+ if (IsPlayerStuck(client))
+ {
+ return;
+ }
+
+ if (!virtualButtonsLocked[client] &&
+ lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount())
+ {
+ Movement_GetOrigin(client, virtualEndOrigin[client]);
+ virtualEndCourse[client] = course;
+ hasVirtualEndButton[client] = true;
+ }
+}
+
+void OnPlayerRunCmdPost_VirtualButtons(int client, int buttons, int cmdnum)
+{
+ CheckForAndHandleUsage(client, buttons);
+ UpdateIndicators(client, cmdnum);
+}
+
+void OnCountedTeleport_VirtualButtons(int client)
+{
+ hasTeleportedSincePressingUse[client] = true;
+}
+
+void OnTeleport_DelayVirtualButtons(int client)
+{
+ lastTeleportTick[client] = GetGameTickCount();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CheckForAndHandleUsage(int client, int buttons)
+{
+ if (buttons & IN_USE && !(gI_OldButtons[client] & IN_USE))
+ {
+ lastUsePressTime[client] = GetGameTime();
+ hasEndedTimerSincePressingUse[client] = false;
+ hasTeleportedSincePressingUse[client] = false;
+ onlyNaturalButtonPressed[client] = startTimerButtonPressTick[client] == GetGameTickCount();
+ }
+
+ bool useCheck = PassesUseCheck(client);
+
+ // Start button
+ if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) == TimerButtonZoneType_BothZones)
+ && GetHasVirtualStartButton(client) && InRangeOfVirtualStart(client) && CanReachVirtualStart(client))
+ {
+ if (TimerStart(client, virtualStartCourse[client], .playSound = false))
+ {
+ startedTimerLastTick[client] = true;
+ OnVirtualStartButtonPress_Teleports(client);
+ }
+ }
+ else if (startedTimerLastTick[client])
+ {
+ // Without that check you get two sounds when pressing the natural timer button
+ if (!onlyNaturalButtonPressed[client])
+ {
+ PlayTimerStartSound(client);
+ }
+ onlyNaturalButtonPressed[client] = false;
+ startedTimerLastTick[client] = false;
+ }
+
+ // End button
+ if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) != TimerButtonZoneType_BothButtons)
+ && GetHasVirtualEndButton(client) && InRangeOfVirtualEnd(client) && CanReachVirtualEnd(client))
+ {
+ if (!wasInEndZone[client])
+ {
+ TimerEnd(client, virtualEndCourse[client]);
+ hasEndedTimerSincePressingUse[client] = true; // False end counts as well
+ wasInEndZone[client] = true;
+ }
+ }
+ else
+ {
+ wasInEndZone[client] = false;
+ }
+}
+
+static bool PassesUseCheck(int client)
+{
+ if (GetGameTime() - lastUsePressTime[client] < GOKZ_VIRTUAL_BUTTON_USE_DETECTION_TIME + EPSILON
+ && !hasEndedTimerSincePressingUse[client]
+ && !hasTeleportedSincePressingUse[client])
+ {
+ return true;
+ }
+
+ return false;
+}
+
+bool InRangeOfVirtualStart(int client)
+{
+ return InRangeOfButton(client, virtualStartOrigin[client]);
+}
+
+static bool InRangeOfVirtualEnd(int client)
+{
+ return InRangeOfButton(client, virtualEndOrigin[client]);
+}
+
+static bool InRangeOfButton(int client, const float buttonOrigin[3])
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ float distanceToButton = GetVectorDistance(origin, buttonOrigin);
+ return distanceToButton <= gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)];
+}
+
+bool CanReachVirtualStart(int client)
+{
+ return CanReachButton(client, virtualStartOrigin[client]);
+}
+
+static bool CanReachVirtualEnd(int client)
+{
+ return CanReachButton(client, virtualEndOrigin[client]);
+}
+
+static bool CanReachButton(int client, const float buttonOrigin[3])
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ Handle trace = TR_TraceRayFilterEx(origin, buttonOrigin, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers);
+ bool didHit = TR_DidHit(trace);
+ delete trace;
+ return !didHit;
+}
+
+
+
+// ===== [ INDICATOR ] =====
+
+static void UpdateIndicators(int client, int cmdnum)
+{
+ if (cmdnum % 128 != 0 || !IsPlayerAlive(client)
+ || GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled)
+ {
+ return;
+ }
+
+ if (hasVirtualStartButton[client])
+ {
+ DrawIndicator(client, virtualStartOrigin[client], { 0, 255, 0, 255 } );
+ }
+
+ if (hasVirtualEndButton[client])
+ {
+ DrawIndicator(client, virtualEndOrigin[client], { 255, 0, 0, 255 } );
+ }
+}
+
+static void DrawIndicator(int client, const float origin[3], const int colour[4])
+{
+ float radius = gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)];
+ if (radius <= EPSILON) // Don't draw circle of radius 0
+ {
+ return;
+ }
+
+ float x, y, start[3], end[3];
+
+ // Create the start position for the first part of the beam
+ start[0] = origin[0] + radius;
+ start[1] = origin[1];
+ start[2] = origin[2];
+
+ for (int i = 1; i <= 31; i++) // Circle is broken into 31 segments
+ {
+ float angle = 2 * PI / 31 * i;
+ x = radius * Cosine(angle);
+ y = radius * Sine(angle);
+
+ end[0] = origin[0] + x;
+ end[1] = origin[1] + y;
+ end[2] = origin[2];
+
+ TE_SetupBeamPoints(start, end, beamSprite, haloSprite, 0, 0, 0.97, 0.2, 0.2, 0, 0.0, colour, 0);
+ TE_SendToClient(client);
+
+ start[0] = end[0];
+ start[1] = end[1];
+ start[2] = end[2];
+ }
+}
diff --git a/sourcemod/scripting/gokz-core/triggerfix.sp b/sourcemod/scripting/gokz-core/triggerfix.sp
new file mode 100644
index 0000000..424928d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/triggerfix.sp
@@ -0,0 +1,622 @@
+
+
+// Credits:
+// RNGFix made by rio https://github.com/jason-e/rngfix
+
+
+// Engine constants, NOT settings (do not change)
+#define LAND_HEIGHT 2.0 // Maximum height above ground at which you can "land"
+#define MIN_STANDABLE_ZNRM 0.7 // Minimum surface normal Z component of a walkable surface
+
+static int processMovementTicks[MAXPLAYERS+1];
+static float playerFrameTime[MAXPLAYERS+1];
+
+static bool touchingTrigger[MAXPLAYERS+1][2048];
+static int triggerTouchFired[MAXPLAYERS+1][2048];
+static int lastGroundEnt[MAXPLAYERS + 1];
+static bool duckedLastTick[MAXPLAYERS + 1];
+static bool mapTeleportedSequentialTicks[MAXPLAYERS+1];
+static bool jumpBugged[MAXPLAYERS + 1];
+static float jumpBugOrigin[MAXPLAYERS + 1][3];
+
+static ConVar cvGravity;
+
+static Handle acceptInputHookPre;
+static Handle processMovementHookPre;
+static Address serverGameEnts;
+static Handle markEntitiesAsTouching;
+static Handle passesTriggerFilters;
+
+public void OnPluginStart_Triggerfix()
+{
+ HookEvent("player_jump", Event_PlayerJump);
+
+ cvGravity = FindConVar("sv_gravity");
+ if (cvGravity == null)
+ {
+ SetFailState("Could not find sv_gravity");
+ }
+
+ GameData gamedataConf = LoadGameConfigFile("gokz-core.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-core gamedata");
+ }
+
+ // PassesTriggerFilters
+ StartPrepSDKCall(SDKCall_Entity);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "CBaseTrigger::PassesTriggerFilters"))
+ {
+ SetFailState("Failed to get CBaseTrigger::PassesTriggerFilters offset");
+ }
+ PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer);
+ passesTriggerFilters = EndPrepSDKCall();
+
+ if (passesTriggerFilters == null) SetFailState("Unable to prepare SDKCall for CBaseTrigger::PassesTriggerFilters");
+
+ // CreateInterface
+ // Thanks SlidyBat and ici
+ StartPrepSDKCall(SDKCall_Static);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Signature, "CreateInterface"))
+ {
+ SetFailState("Failed to get CreateInterface");
+ }
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+ Handle CreateInterface = EndPrepSDKCall();
+
+ if (CreateInterface == null)
+ {
+ SetFailState("Unable to prepare SDKCall for CreateInterface");
+ }
+
+ char interfaceName[64];
+
+ // ProcessMovement
+ if (!GameConfGetKeyValue(gamedataConf, "IGameMovement", interfaceName, sizeof(interfaceName)))
+ {
+ SetFailState("Failed to get IGameMovement interface name");
+ }
+ Address IGameMovement = SDKCall(CreateInterface, interfaceName, 0);
+ if (!IGameMovement)
+ {
+ SetFailState("Failed to get IGameMovement pointer");
+ }
+
+ int offset = GameConfGetOffset(gamedataConf, "ProcessMovement");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get ProcessMovement offset");
+ }
+
+ processMovementHookPre = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPre);
+ DHookAddParam(processMovementHookPre, HookParamType_CBaseEntity);
+ DHookAddParam(processMovementHookPre, HookParamType_ObjectPtr);
+ DHookRaw(processMovementHookPre, false, IGameMovement);
+
+ // MarkEntitiesAsTouching
+ if (!GameConfGetKeyValue(gamedataConf, "IServerGameEnts", interfaceName, sizeof(interfaceName)))
+ {
+ SetFailState("Failed to get IServerGameEnts interface name");
+ }
+ serverGameEnts = SDKCall(CreateInterface, interfaceName, 0);
+ if (!serverGameEnts)
+ {
+ SetFailState("Failed to get IServerGameEnts pointer");
+ }
+
+ StartPrepSDKCall(SDKCall_Raw);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "IServerGameEnts::MarkEntitiesAsTouching"))
+ {
+ SetFailState("Failed to get IServerGameEnts::MarkEntitiesAsTouching offset");
+ }
+ PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer);
+ markEntitiesAsTouching = EndPrepSDKCall();
+
+ if (markEntitiesAsTouching == null)
+ {
+ SetFailState("Unable to prepare SDKCall for IServerGameEnts::MarkEntitiesAsTouching");
+ }
+
+ gamedataConf = LoadGameConfigFile("sdktools.games/engine.csgo");
+ offset = gamedataConf.GetOffset("AcceptInput");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get AcceptInput offset");
+ }
+
+ acceptInputHookPre = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, DHooks_AcceptInput);
+ DHookAddParam(acceptInputHookPre, HookParamType_CharPtr);
+ DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity);
+ DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity);
+ //varaint_t is a union of 12 (float[3]) plus two int type params 12 + 8 = 20
+ DHookAddParam(acceptInputHookPre, HookParamType_Object, 20, DHookPass_ByVal|DHookPass_ODTOR|DHookPass_OCTOR|DHookPass_OASSIGNOP);
+ DHookAddParam(acceptInputHookPre, HookParamType_Int);
+
+ delete CreateInterface;
+ delete gamedataConf;
+
+ if (gB_LateLoad)
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client)) OnClientPutInServer(client);
+ }
+
+ char classname[64];
+ for (int entity = MaxClients+1; entity < sizeof(touchingTrigger[]); entity++)
+ {
+ if (!IsValidEntity(entity)) continue;
+ GetEntPropString(entity, Prop_Data, "m_iClassname", classname, sizeof(classname));
+ HookTrigger(entity, classname);
+ }
+ }
+}
+
+public void OnEntityCreated_Triggerfix(int entity, const char[] classname)
+{
+ if (entity >= sizeof(touchingTrigger[]))
+ {
+ return;
+ }
+ HookTrigger(entity, classname);
+}
+
+public void OnClientConnected_Triggerfix(int client)
+{
+ processMovementTicks[client] = 0;
+ for (int i = 0; i < sizeof(touchingTrigger[]); i++)
+ {
+ touchingTrigger[client][i] = false;
+ triggerTouchFired[client][i] = 0;
+ }
+}
+
+public void OnClientPutInServer_Triggerfix(int client)
+{
+ SDKHook(client, SDKHook_PostThink, Hook_PlayerPostThink);
+ DHookEntity(acceptInputHookPre, false, client);
+}
+
+public void OnGameFrame_Triggerfix()
+{
+ // Loop through all the players and make sure that triggers that are supposed to be fired but weren't now
+ // get fired properly.
+ // This must be run OUTSIDE of usercmd, because sometimes usercmd gets delayed heavily.
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && IsPlayerAlive(client) && !CheckWater(client) &&
+ (GetEntityMoveType(client) == MOVETYPE_WALK || GetEntityMoveType(client) == MOVETYPE_LADDER))
+ {
+ DoTriggerFix(client);
+
+ // Reset the Touch tracking.
+ // We save a bit of performance by putting this inside the loop
+ // Even if triggerTouchFired is not correct, touchingTrigger still is.
+ // That should prevent DoTriggerFix from activating the wrong triggers.
+ // Plus, players respawn where they previously are as well with a timer on,
+ // so this should not be a big problem.
+ for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++)
+ {
+ triggerTouchFired[client][trigger] = 0;
+ }
+ }
+ }
+}
+
+void OnPlayerRunCmd_Triggerfix(int client)
+{
+ // Reset the Touch tracking.
+ // While this is mostly unnecessary, it can also happen that the server runs multiple ticks of player movement at once,
+ // therefore the triggers need to be checked again.
+ for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++)
+ {
+ triggerTouchFired[client][trigger] = 0;
+ }
+}
+
+static void Event_PlayerJump(Event event, const char[] name, bool dontBroadcast)
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+
+ jumpBugged[client] = !!lastGroundEnt[client];
+ if (jumpBugged[client])
+ {
+ GetClientAbsOrigin(client, jumpBugOrigin[client]);
+ // if player's origin is still in the ducking position then adjust for that.
+ if (duckedLastTick[client] && !Movement_GetDucking(client))
+ {
+ jumpBugOrigin[client][2] -= 9.0;
+ }
+ }
+}
+
+static Action Hook_TriggerStartTouch(int entity, int other)
+{
+ if (1 <= other <= MaxClients)
+ {
+ touchingTrigger[other][entity] = true;
+ }
+
+ return Plugin_Continue;
+}
+
+static Action Hook_TriggerEndTouch(int entity, int other)
+{
+ if (1 <= other <= MaxClients)
+ {
+ touchingTrigger[other][entity] = false;
+ }
+ return Plugin_Continue;
+}
+
+static Action Hook_TriggerTouch(int entity, int other)
+{
+ if (1 <= other <= MaxClients)
+ {
+ triggerTouchFired[other][entity]++;
+ }
+ return Plugin_Continue;
+}
+
+static MRESReturn DHook_ProcessMovementPre(Handle hParams)
+{
+ int client = DHookGetParam(hParams, 1);
+
+ processMovementTicks[client]++;
+ playerFrameTime[client] = GetTickInterval() * GetEntPropFloat(client, Prop_Data, "m_flLaggedMovementValue");
+ mapTeleportedSequentialTicks[client] = false;
+
+ if (IsPlayerAlive(client))
+ {
+ if (GetEntityMoveType(client) == MOVETYPE_WALK
+ && !CheckWater(client))
+ {
+ lastGroundEnt[client] = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity");
+ }
+ duckedLastTick[client] = Movement_GetDucking(client);
+ }
+
+ return MRES_Ignored;
+}
+
+static MRESReturn DHooks_AcceptInput(int client, DHookReturn hReturn, DHookParam hParams)
+{
+ if (!IsValidClient(client) || !IsPlayerAlive(client) || CheckWater(client) ||
+ (GetEntityMoveType(client) != MOVETYPE_WALK && GetEntityMoveType(client) != MOVETYPE_LADDER))
+ {
+ return MRES_Ignored;
+ }
+
+ // Get args
+ static char param[64];
+ static char command[64];
+ DHookGetParamString(hParams, 1, command, sizeof(command));
+ if (StrEqual(command, "AddOutput"))
+ {
+ DHookGetParamObjectPtrString(hParams, 4, 0, ObjectValueType_String, param, sizeof(param));
+ char kv[16];
+ SplitString(param, " ", kv, sizeof(kv));
+ // KVs are case insensitive.
+ // Any of these inputs can change the filter behavior.
+ if (StrEqual(kv[0], "targetname", false) || StrEqual(kv[0], "teamnumber", false) || StrEqual(kv[0], "classname", false) || StrEqual(command, "ResponseContext", false))
+ {
+ DoTriggerFix(client, true);
+ }
+ }
+ else if (StrEqual(command, "AddContext") || StrEqual(command, "RemoveContext") || StrEqual(command, "ClearContext"))
+ {
+ DoTriggerFix(client, true);
+ }
+ return MRES_Ignored;
+}
+
+static bool DoTriggerFix(int client, bool filterFix = false)
+{
+ // Adapted from DoTriggerjumpFix right below.
+ float landingMins[3], landingMaxs[3];
+ float origin[3];
+
+ GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+ GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins);
+ GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs);
+
+ ArrayList triggers = new ArrayList();
+ // Get a list of triggers that we are touching now.
+
+ TR_EnumerateEntitiesHull(origin, origin, landingMins, landingMaxs, true, AddTrigger, triggers);
+
+ bool didSomething = false;
+
+ for (int i = 0; i < triggers.Length; i++)
+ {
+ int trigger = triggers.Get(i);
+ if (!touchingTrigger[client][trigger])
+ {
+ // Normally this wouldn't happen, because the trigger should be colliding with the player's hull if it gets here.
+ continue;
+ }
+ char className[64];
+ GetEntityClassname(trigger, className, sizeof(className));
+ if (StrEqual(className, "trigger_push"))
+ {
+ // Completely ignore push triggers.
+ continue;
+ }
+ if (filterFix && SDKCall(passesTriggerFilters, trigger, client) && triggerTouchFired[client][trigger] < GOKZ_MAX_RETOUCH_TRIGGER_COUNT)
+ {
+ // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick.
+ SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger);
+
+ // Player properties might be changed right after this so it will need to be triggered again.
+ // Triggers changing this filter will loop onto itself infintely so we need to avoid that.
+ triggerTouchFired[client][trigger]++;
+ didSomething = true;
+ }
+ else if (!triggerTouchFired[client][trigger])
+ {
+ // If the player is still touching the trigger on this tick, and Touch was not called for whatever reason
+ // in the last tick, we make sure that it is called now.
+ SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger);
+ triggerTouchFired[client][trigger]++;
+ didSomething = true;
+ }
+ }
+
+ delete triggers;
+
+ return didSomething;
+}
+
+static bool DoTriggerjumpFix(int client, const float landingPoint[3], const float landingMins[3], const float landingMaxs[3])
+{
+ // It's possible to land above a trigger but also in another trigger_teleport, have the teleport move you to
+ // another location, and then the trigger jumping fix wouldn't fire the other trigger you technically landed above,
+ // but I can't imagine a mapper would ever actually stack triggers like that.
+
+ float origin[3];
+ GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+
+ float landingMaxsBelow[3];
+ landingMaxsBelow[0] = landingMaxs[0];
+ landingMaxsBelow[1] = landingMaxs[1];
+ landingMaxsBelow[2] = origin[2] - landingPoint[2];
+
+ ArrayList triggers = new ArrayList();
+
+ // Find triggers that are between us and the ground (using the bounding box quadrant we landed with if applicable).
+ // This will fail on triggers thinner than 0.03125 unit thick, but it's highly unlikely that a mapper would put a trigger that thin.
+ TR_EnumerateEntitiesHull(landingPoint, landingPoint, landingMins, landingMaxsBelow, true, AddTrigger, triggers);
+
+ bool didSomething = false;
+
+ for (int i = 0; i < triggers.Length; i++)
+ {
+ int trigger = triggers.Get(i);
+
+ // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick.
+ // In case that could cause side-effects, manually keep track of triggers we are actually touching
+ // and don't re-touch them.
+ if (touchingTrigger[client][trigger])
+ {
+ continue;
+ }
+
+ SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger);
+ didSomething = true;
+ }
+
+ delete triggers;
+
+ return didSomething;
+}
+
+// PostThink works a little better than a ProcessMovement post hook because we need to wait for ProcessImpacts (trigger activation)
+static void Hook_PlayerPostThink(int client)
+{
+ if (!IsPlayerAlive(client)
+ || GetEntityMoveType(client) != MOVETYPE_WALK
+ || CheckWater(client))
+ {
+ return;
+ }
+
+ bool landed = (GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") != -1
+ && lastGroundEnt[client] == -1)
+ || jumpBugged[client];
+
+ float landingMins[3], landingMaxs[3], landingPoint[3];
+
+ // Get info about the ground we landed on (if we need to do landing fixes).
+ if (landed)
+ {
+ float origin[3], nrm[3], velocity[3];
+ GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+ GetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity);
+
+ if (jumpBugged[client])
+ {
+ origin = jumpBugOrigin[client];
+ }
+
+ GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins);
+ GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs);
+
+ float originBelow[3];
+ originBelow[0] = origin[0];
+ originBelow[1] = origin[1];
+ originBelow[2] = origin[2] - LAND_HEIGHT;
+
+ TR_TraceHullFilter(origin, originBelow, landingMins, landingMaxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (!TR_DidHit())
+ {
+ // This should never happen, since we know we are on the ground.
+ landed = false;
+ }
+ else
+ {
+ TR_GetPlaneNormal(null, nrm);
+
+ if (nrm[2] < MIN_STANDABLE_ZNRM)
+ {
+ // This is rare, and how the incline fix should behave isn't entirely clear because maybe we should
+ // collide with multiple faces at once in this case, but let's just get the ground we officially
+ // landed on and use that for our ground normal.
+
+ // landingMins and landingMaxs will contain the final values used to find the ground after returning.
+ if (TracePlayerBBoxForGround(origin, originBelow, landingMins, landingMaxs))
+ {
+ TR_GetPlaneNormal(null, nrm);
+ }
+ else
+ {
+ // This should also never happen.
+ landed = false;
+ }
+ }
+
+ TR_GetEndPosition(landingPoint);
+ }
+ }
+
+ // reset it here because we don't need it again
+ jumpBugged[client] = false;
+
+ // Must use TR_DidHit because if the unduck origin is closer than 0.03125 units from the ground,
+ // the trace fraction would return 0.0.
+ if (landed && TR_DidHit())
+ {
+ DoTriggerjumpFix(client, landingPoint, landingMins, landingMaxs);
+ // Check if a trigger we just touched put us in the air (probably due to a teleport).
+ if (GetEntityFlags(client) & FL_ONGROUND == 0)
+ {
+ landed = false;
+ }
+ }
+}
+
+static bool PlayerFilter(int entity, int mask)
+{
+ return !(1 <= entity <= MaxClients);
+}
+
+static void HookTrigger(int entity, const char[] classname)
+{
+ if (StrContains(classname, "trigger_") != -1)
+ {
+ SDKHook(entity, SDKHook_StartTouchPost, Hook_TriggerStartTouch);
+ SDKHook(entity, SDKHook_EndTouchPost, Hook_TriggerEndTouch);
+ SDKHook(entity, SDKHook_TouchPost, Hook_TriggerTouch);
+ }
+}
+
+static bool CheckWater(int client)
+{
+ // The cached water level is updated multiple times per tick, including after movement happens,
+ // so we can just check the cached value here.
+ return GetEntProp(client, Prop_Data, "m_nWaterLevel") > 1;
+}
+
+public bool AddTrigger(int entity, ArrayList triggers)
+{
+ TR_ClipCurrentRayToEntity(MASK_ALL, entity);
+ if (TR_DidHit())
+ {
+ triggers.Push(entity);
+ }
+
+ return true;
+}
+
+static bool TracePlayerBBoxForGround(const float origin[3], const float originBelow[3], float mins[3], float maxs[3])
+{
+ // See CGameMovement::TracePlayerBBoxForGround()
+
+ float origMins[3], origMaxs[3];
+ origMins = mins;
+ origMaxs = maxs;
+
+ float nrm[3];
+
+ mins = origMins;
+
+ // -x -y
+ maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0];
+ maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1];
+ maxs[2] = origMaxs[2];
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ // +x +y
+ mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0];
+ mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1];
+ mins[2] = origMins[2];
+
+ maxs = origMaxs;
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ // -x +y
+ mins[0] = origMins[0];
+ mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1];
+ mins[2] = origMins[2];
+
+ maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0];
+ maxs[1] = origMaxs[1];
+ maxs[2] = origMaxs[2];
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ // +x -y
+ mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0];
+ mins[1] = origMins[1];
+ mins[2] = origMins[2];
+
+ maxs[0] = origMaxs[0];
+ maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1];
+ maxs[2] = origMaxs[2];
+
+ TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter);
+
+ if (TR_DidHit())
+ {
+ TR_GetPlaneNormal(null, nrm);
+ if (nrm[2] >= MIN_STANDABLE_ZNRM)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/sourcemod/scripting/gokz-errorboxfixer.sp b/sourcemod/scripting/gokz-errorboxfixer.sp
new file mode 100644
index 0000000..fd3f76a
--- /dev/null
+++ b/sourcemod/scripting/gokz-errorboxfixer.sp
@@ -0,0 +1,89 @@
+#include <sourcemod>
+#include <sdktools>
+
+#include <gokz>
+
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+public Plugin myinfo =
+{
+ name = "GOKZ KZErrorBoxFixer",
+ author = "1NutWunDeR",
+ description = "Adds missing models for KZ maps",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-errorboxfixer.txt"
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnMapStart()
+{
+ AddFileToDownloadsTable("models/kzmod/buttons/stand_button.vtf");
+ AddFileToDownloadsTable("models/kzmod/buttons/stand_button.vmt");
+ AddFileToDownloadsTable("models/kzmod/buttons/stand_button_normal.vtf");
+ AddFileToDownloadsTable("models/kzmod/buttons/standing_button.mdl");
+ AddFileToDownloadsTable("models/kzmod/buttons/standing_button.dx90.vtx");
+ AddFileToDownloadsTable("models/kzmod/buttons/standing_button.phy");
+ AddFileToDownloadsTable("models/kzmod/buttons/standing_button.vvd");
+ AddFileToDownloadsTable("models/kzmod/buttons/stone_button.mdl");
+ AddFileToDownloadsTable("models/kzmod/buttons/stone_button.dx90.vtx");
+ AddFileToDownloadsTable("models/kzmod/buttons/stone_button.phy");
+ AddFileToDownloadsTable("models/kzmod/buttons/stone_button.vvd");
+ AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.mdl");
+ AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.dx90.vtx");
+ AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.phy");
+ AddFileToDownloadsTable("models/props_wasteland/pipecluster002a.vvd");
+ AddFileToDownloadsTable("materials/kzmod/starttimersign.vmt");
+ AddFileToDownloadsTable("materials/kzmod/starttimersign.vtf");
+ AddFileToDownloadsTable("materials/kzmod/stoptimersign.vmt");
+ AddFileToDownloadsTable("materials/kzmod/stoptimersign.vtf");
+ AddFileToDownloadsTable("models/props/switch001.mdl");
+ AddFileToDownloadsTable("models/props/switch001.vvd");
+ AddFileToDownloadsTable("models/props/switch001.phy");
+ AddFileToDownloadsTable("models/props/switch001.vtx");
+ AddFileToDownloadsTable("models/props/switch001.dx90.vtx");
+ AddFileToDownloadsTable("materials/models/props/switch.vmt");
+ AddFileToDownloadsTable("materials/models/props/switch.vtf");
+ AddFileToDownloadsTable("materials/models/props/switch001.vmt");
+ AddFileToDownloadsTable("materials/models/props/switch001.vtf");
+ AddFileToDownloadsTable("materials/models/props/switch001_normal.vmt");
+ AddFileToDownloadsTable("materials/models/props/switch001_normal.vtf");
+ AddFileToDownloadsTable("materials/models/props/switch001_lightwarp.vmt");
+ AddFileToDownloadsTable("materials/models/props/switch001_lightwarp.vtf");
+ AddFileToDownloadsTable("materials/models/props/switch001_exponent.vmt");
+ AddFileToDownloadsTable("materials/models/props/switch001_exponent.vtf");
+ AddFileToDownloadsTable("materials/models/props/startkztimer.vmt");
+ AddFileToDownloadsTable("materials/models/props/startkztimer.vtf");
+ AddFileToDownloadsTable("materials/models/props/stopkztimer.vmt");
+ AddFileToDownloadsTable("materials/models/props/stopkztimer.vtf");
+
+ PrecacheModel("models/kzmod/buttons/stand_button.vmt", true);
+ PrecacheModel("models/props_wasteland/pipecluster002a.mdl", true);
+ PrecacheModel("models/kzmod/buttons/standing_button.mdl", true);
+ PrecacheModel("models/kzmod/buttons/stone_button.mdl", true);
+ PrecacheModel("materials/kzmod/starttimersign.vmt", true);
+ PrecacheModel("materials/kzmod/stoptimersign.vmt", true);
+ PrecacheModel("models/props/switch001.mdl", true);
+ PrecacheModel("materials/models/props/startkztimer.vmt", true);
+ PrecacheModel("materials/models/props/stopkztimer.vmt", true);
+}
diff --git a/sourcemod/scripting/gokz-global.sp b/sourcemod/scripting/gokz-global.sp
new file mode 100644
index 0000000..79f8b6a
--- /dev/null
+++ b/sourcemod/scripting/gokz-global.sp
@@ -0,0 +1,740 @@
+#include <sourcemod>
+
+#include <sdktools>
+
+#include <GlobalAPI>
+#include <gokz/anticheat>
+#include <gokz/core>
+#include <gokz/global>
+#include <gokz/replays>
+#include <gokz/momsurffix>
+
+#include <autoexecconfig>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/localdb>
+#include <gokz/localranks>
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Global",
+ author = "DanZay",
+ description = "Provides centralised records and bans via GlobalAPI",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-global.txt"
+
+bool gB_GOKZLocalDB;
+
+bool gB_APIKeyCheck;
+bool gB_ModeCheck[MODE_COUNT];
+bool gB_BannedCommandsCheck;
+char gC_CurrentMap[64];
+int gI_CurrentMapFileSize;
+bool gB_InValidRun[MAXPLAYERS + 1];
+bool gB_GloballyVerified[MAXPLAYERS + 1];
+bool gB_EnforcerOnFreshMap;
+bool gB_JustLateLoaded;
+int gI_FPSMax[MAXPLAYERS + 1];
+bool gB_waitingForFPSKick[MAXPLAYERS + 1];
+bool gB_MapValidated;
+int gI_MapID;
+int gI_MapFileSize;
+int gI_MapTier;
+
+ConVar gCV_gokz_settings_enforcer;
+ConVar gCV_gokz_warn_for_non_global_map;
+ConVar gCV_EnforcedCVar[ENFORCEDCVAR_COUNT];
+
+#include "gokz-global/api.sp"
+#include "gokz-global/ban_player.sp"
+#include "gokz-global/commands.sp"
+#include "gokz-global/maptop_menu.sp"
+#include "gokz-global/print_records.sp"
+#include "gokz-global/send_run.sp"
+#include "gokz-global/points.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-global");
+ gB_JustLateLoaded = late;
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ if (FloatAbs(1.0 / GetTickInterval() - 128.0) > EPSILON)
+ {
+ SetFailState("gokz-global currently only supports 128 tickrate servers.");
+ }
+ if (FindCommandLineParam("-insecure") || FindCommandLineParam("-tools"))
+ {
+ SetFailState("gokz-global currently only supports VAC-secured servers.");
+ }
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-global.phrases");
+
+ gB_APIKeyCheck = false;
+ gB_MapValidated = false;
+ gI_MapID = -1;
+ gI_MapFileSize = -1;
+ gI_MapTier = -1;
+
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ gB_ModeCheck[mode] = false;
+ }
+
+ CreateConVars();
+ CreateGlobalForwards();
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZLocalDB = LibraryExists("gokz-localdb");
+
+ 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");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb");
+}
+
+Action IntegrityChecks(Handle timer)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ QueryClientConVar(client, "fps_max", FPSCheck, client);
+ QueryClientConVar(client, "m_yaw", MYAWCheck, client);
+ }
+ }
+
+ for (int i = 0; i < BANNEDPLUGINCOMMAND_COUNT; i++)
+ {
+ if (CommandExists(gC_BannedPluginCommands[i]))
+ {
+ Handle bannedIterator = GetPluginIterator();
+ char pluginName[128];
+ bool foundPlugin = false;
+ while (MorePlugins(bannedIterator))
+ {
+ Handle bannedPlugin = ReadPlugin(bannedIterator);
+ GetPluginInfo(bannedPlugin, PlInfo_Name, pluginName, sizeof(pluginName));
+ if (StrEqual(pluginName, gC_BannedPlugins[i]))
+ {
+ char pluginPath[128];
+ GetPluginFilename(bannedPlugin, pluginPath, sizeof(pluginPath));
+ ServerCommand("sm plugins unload %s", pluginPath);
+ char disabledPath[256], enabledPath[256], pluginFile[4][128];
+ int subfolders = ExplodeString(pluginPath, "/", pluginFile, sizeof(pluginFile), sizeof(pluginFile[]));
+ BuildPath(Path_SM, disabledPath, sizeof(disabledPath), "plugins/disabled/%s", pluginFile[subfolders - 1]);
+ BuildPath(Path_SM, enabledPath, sizeof(enabledPath), "plugins/%s", pluginPath);
+ RenameFile(disabledPath, enabledPath);
+ LogError("[KZ] %s cannot be loaded at the same time as gokz-global. %s has been disabled.", pluginName, pluginName);
+ delete bannedPlugin;
+ foundPlugin = true;
+ break;
+ }
+ delete bannedPlugin;
+ }
+ if (!foundPlugin && gB_BannedCommandsCheck)
+ {
+ gB_BannedCommandsCheck = false;
+ LogError("You can't have a plugin which implements the %s command. Please disable it and reload the map.", gC_BannedPluginCommands[i]);
+ }
+ delete bannedIterator;
+ }
+ }
+
+ return Plugin_Handled;
+}
+
+public void FPSCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value)
+{
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ gI_FPSMax[client] = StringToInt(cvarValue);
+ if (gI_FPSMax[client] > 0 && gI_FPSMax[client] < GL_FPS_MAX_MIN_VALUE)
+ {
+ if (!gB_waitingForFPSKick[client])
+ {
+ gB_waitingForFPSKick[client] = true;
+ CreateTimer(GL_FPS_MAX_KICK_TIMEOUT, FPSKickPlayer, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE);
+ GOKZ_PrintToChat(client, true, "%t", "Warn Player fps_max");
+ if (GOKZ_GetTimerRunning(client))
+ {
+ GOKZ_StopTimer(client, true);
+ }
+ else
+ {
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+ }
+ }
+ }
+ else
+ {
+ gB_waitingForFPSKick[client] = false;
+ }
+ }
+}
+
+public void MYAWCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value)
+{
+ if (IsValidClient(client) && !IsFakeClient(client) && StringToFloat(cvarValue) > GL_MYAW_MAX_VALUE)
+ {
+ KickClient(client, "%T", "Kick Player m_yaw", client);
+ }
+}
+
+Action FPSKickPlayer(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client) && !IsFakeClient(client) && gB_waitingForFPSKick[client])
+ {
+ KickClient(client, "%T", "Kick Player fps_max", client);
+ }
+
+ return Plugin_Handled;
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ gB_GloballyVerified[client] = false;
+ gB_waitingForFPSKick[client] = false;
+ OnClientPutInServer_PrintRecords(client);
+}
+
+// OnClientAuthorized is apparently too early
+public void OnClientPostAdminCheck(int client)
+{
+ ResetPoints(client);
+
+ if (GlobalAPI_IsInit() && !IsFakeClient(client))
+ {
+ CheckClientGlobalBan(client);
+ UpdatePoints(client);
+ }
+}
+
+public void GlobalAPI_OnInitialized()
+{
+ SetupAPI();
+}
+
+
+public Action GOKZ_OnTimerStart(int client, int course)
+{
+ KZPlayer player = KZPlayer(client);
+ int mode = player.Mode;
+
+ // We check the timer running to prevent spam when standing inside VB.
+ if (gCV_gokz_warn_for_non_global_map.BoolValue
+ && GlobalAPI_HasAPIKey()
+ && !GlobalsEnabled(mode)
+ && !GOKZ_GetTimerRunning(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Warn Player Not Global Run");
+ }
+
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnTimerStart_Post(int client, int course)
+{
+ KZPlayer player = KZPlayer(client);
+ int mode = player.Mode;
+ gB_InValidRun[client] = GlobalsEnabled(mode);
+}
+
+public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ if (gB_GloballyVerified[client] && gB_InValidRun[client])
+ {
+ SendTime(client, course, time, teleportsUsed);
+ }
+}
+
+public Action GOKZ_RP_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay)
+{
+ if (gB_GloballyVerified[client] && gB_InValidRun[client])
+ {
+ OnReplaySaved_SendReplay(client, replayType, map, course, timeType, time, filePath, tempReplay);
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnRunInvalidated(int client)
+{
+ gB_InValidRun[client] = false;
+}
+
+public void GOKZ_GL_OnNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall, float runTime)
+{
+ AnnounceNewTopTime(client, course, mode, timeType, rank, rankOverall);
+}
+
+public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats)
+{
+ if (!gB_GloballyVerified[client])
+ {
+ return;
+ }
+
+ GlobalBanPlayer(client, reason, notes, stats);
+ gB_GloballyVerified[client] = false;
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ LoadSounds();
+
+ GetCurrentMapDisplayName(gC_CurrentMap, sizeof(gC_CurrentMap));
+ gI_CurrentMapFileSize = GetCurrentMapFileSize();
+
+ gB_BannedCommandsCheck = true;
+
+ // Prevent just reloading the plugin after messing with the map
+ if (gB_JustLateLoaded)
+ {
+ gB_JustLateLoaded = false;
+ }
+ else
+ {
+ gB_EnforcerOnFreshMap = true;
+ }
+
+ // In case of late loading
+ if (GlobalAPI_IsInit())
+ {
+ GlobalAPI_OnInitialized();
+ }
+
+ // Setup a timer to monitor server/client integrity
+ CreateTimer(1.0, IntegrityChecks, INVALID_HANDLE, TIMER_FLAG_NO_MAPCHANGE | TIMER_REPEAT);
+}
+
+public void OnMapEnd()
+{
+ // So it doesn't get carried over to the next map
+ gI_MapID = -1;
+ for (int client = 1; client < MaxClients; client++)
+ {
+ ResetMapPoints(client);
+ }
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, gC_CoreOptionNames[Option_Mode])
+ && GlobalAPI_IsInit())
+ {
+ UpdatePoints(client);
+ }
+}
+
+public void GOKZ_OnModeUnloaded(int mode)
+{
+ gB_ModeCheck[mode] = false;
+}
+
+public Action GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client)
+{
+ char pluginName[64];
+ GetPluginInfo(plugin, PlInfo_Name, pluginName, sizeof(pluginName));
+ if (GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ LogMessage("Invalidated %N's run as gokz-core native was called by \"%s\"", client, pluginName);
+ }
+ GOKZ_InvalidateRun(client);
+ return Plugin_Continue;
+}
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GlobalsEnabled(int mode)
+{
+ return gB_APIKeyCheck && gB_BannedCommandsCheck && gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap && MapCheck() && gB_ModeCheck[mode];
+}
+
+bool MapCheck()
+{
+ return gB_MapValidated
+ && gI_MapID > 0
+ && gI_MapFileSize == gI_CurrentMapFileSize;
+}
+
+void PrintGlobalCheckToChat(int client)
+{
+ GOKZ_PrintToChat(client, true, "%t", "Global Check Header");
+ GOKZ_PrintToChat(client, false, "%t", "Global Check",
+ gB_APIKeyCheck ? "{green}✓" : "{darkred}X",
+ gB_BannedCommandsCheck ? "{green}✓" : "{darkred}X",
+ gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap ? "{green}✓" : "{darkred}X",
+ MapCheck() ? "{green}✓" : "{darkred}X",
+ gB_GloballyVerified[client] ? "{green}✓" : "{darkred}X");
+
+ char modeCheck[256];
+ FormatEx(modeCheck, sizeof(modeCheck), "{purple}%s %s", gC_ModeNames[0], gB_ModeCheck[0] ? "{green}✓" : "{darkred}X");
+ for (int i = 1; i < MODE_COUNT; i++)
+ {
+ FormatEx(modeCheck, sizeof(modeCheck), "%s {grey}| {purple}%s %s", modeCheck, gC_ModeNames[i], gB_ModeCheck[i] ? "{green}✓" : "{darkred}X");
+ }
+ GOKZ_PrintToChat(client, false, "%s", modeCheck);
+}
+
+void AnnounceNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall)
+{
+ bool newRecord = false;
+
+ if (timeType == TimeType_Nub && rankOverall != 0)
+ {
+ if (rankOverall == 1)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Record (NUB)", client, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Bonus Record (NUB)", client, course, gC_ModeNamesShort[mode]);
+ }
+ newRecord = true;
+ }
+ else
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Top Time (NUB)", client, rankOverall, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Top Bonus Time (NUB)", client, rankOverall, course, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+ else if (timeType == TimeType_Pro)
+ {
+ if (rankOverall != 0)
+ {
+ if (rankOverall == 1)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Record (NUB)", client, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Bonus Record (NUB)", client, course, gC_ModeNamesShort[mode]);
+ }
+ newRecord = true;
+ }
+ else
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Top Time (NUB)", client, rankOverall, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Top Bonus Time (NUB)", client, rankOverall, course, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+
+ if (rank == 1)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Record (PRO)", client, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Bonus Record (PRO)", client, course, gC_ModeNamesShort[mode]);
+ }
+ newRecord = true;
+ }
+ else
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Top Time (PRO)", client, rank, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Global Top Bonus Time (PRO)", client, rank, course, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+
+ if (newRecord)
+ {
+ PlayBeatRecordSound();
+ }
+}
+
+void PlayBeatRecordSound()
+{
+ GOKZ_EmitSoundToAll(GL_SOUND_NEW_RECORD, _, "World Record");
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CreateConVars()
+{
+ AutoExecConfig_SetFile("gokz-global", "sourcemod/gokz");
+ AutoExecConfig_SetCreateFile(true);
+
+ gCV_gokz_settings_enforcer = AutoExecConfig_CreateConVar("gokz_settings_enforcer", "1", "Whether GOKZ enforces convars required for global records.", _, true, 0.0, true, 1.0);
+ gCV_gokz_warn_for_non_global_map = AutoExecConfig_CreateConVar("gokz_warn_for_non_global_map", "1", "Whether or not GOKZ should warn players if the global check does not pass.", _, true, 0.0, true, 1.0);
+ gCV_gokz_settings_enforcer.AddChangeHook(OnConVarChanged);
+
+ AutoExecConfig_ExecuteFile();
+ AutoExecConfig_CleanFile();
+
+ for (int i = 0; i < ENFORCEDCVAR_COUNT; i++)
+ {
+ gCV_EnforcedCVar[i] = FindConVar(gC_EnforcedCVars[i]);
+ gCV_EnforcedCVar[i].FloatValue = gF_EnforcedCVarValues[i];
+ gCV_EnforcedCVar[i].AddChangeHook(OnEnforcedConVarChanged);
+ }
+}
+
+public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ if (convar == gCV_gokz_settings_enforcer)
+ {
+ if (gCV_gokz_settings_enforcer.BoolValue)
+ {
+ for (int i = 0; i < ENFORCEDCVAR_COUNT; i++)
+ {
+ gCV_EnforcedCVar[i].FloatValue = gF_EnforcedCVarValues[i];
+ }
+ }
+ else
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ gB_InValidRun[client] = false;
+ }
+
+ // You have to change map before you can re-activate that
+ gB_EnforcerOnFreshMap = false;
+ }
+ }
+}
+
+public void OnEnforcedConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ if (gCV_gokz_settings_enforcer.BoolValue)
+ {
+ for (int i = 0; i < ENFORCEDCVAR_COUNT; i++)
+ {
+ if (convar == gCV_EnforcedCVar[i])
+ {
+ gCV_EnforcedCVar[i].FloatValue = gF_EnforcedCVarValues[i];
+ return;
+ }
+ }
+ }
+}
+
+static void SetupAPI()
+{
+ GlobalAPI_GetAuthStatus(GetAuthStatusCallback);
+ GlobalAPI_GetModes(GetModeInfoCallback);
+ GlobalAPI_GetMapByName(GetMapCallback, _, gC_CurrentMap);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ CheckClientGlobalBan(client);
+ }
+ }
+}
+
+public int GetAuthStatusCallback(JSON_Object auth_json, GlobalAPIRequestData request)
+{
+ if (request.Failure)
+ {
+ LogError("Failed to check API key with Global API.");
+ return 0;
+ }
+
+ APIAuth auth = view_as<APIAuth>(auth_json);
+ if (!auth.IsValid)
+ {
+ LogError("Global API key was found to be missing or invalid.");
+ }
+ gB_APIKeyCheck = auth.IsValid;
+ return 0;
+}
+
+public int GetModeInfoCallback(JSON_Object modes, GlobalAPIRequestData request)
+{
+ if (request.Failure)
+ {
+ LogError("Failed to check mode versions with Global API.");
+ return 0;
+ }
+
+ if (!modes.IsArray)
+ {
+ LogError("GlobalAPI returned a malformed response while looking up the modes.");
+ return 0;
+ }
+
+ for (int i = 0; i < modes.Length; i++)
+ {
+ APIMode mode = view_as<APIMode>(modes.GetObjectIndexed(i));
+ int mode_id = GOKZ_GL_FromGlobalMode(view_as<GlobalMode>(mode.Id));
+ if (mode_id == -1)
+ {
+ LogError("GlobalAPI returned a malformed mode.");
+ }
+ else if (mode.LatestVersion <= GOKZ_GetModeVersion(mode_id))
+ {
+ gB_ModeCheck[mode_id] = true;
+ }
+ else
+ {
+ char desc[128];
+
+ gB_ModeCheck[mode_id] = false;
+ mode.GetLatestVersionDesc(desc, sizeof(desc));
+ LogError("Global API requires %s mode version %d (%s). You have version %d (%s).",
+ gC_ModeNames[mode_id], mode.LatestVersion, desc, GOKZ_GetModeVersion(mode_id), GOKZ_VERSION);
+ }
+ }
+ return 0;
+}
+
+public int GetMapCallback(JSON_Object map_json, GlobalAPIRequestData request)
+{
+ if (request.Failure || map_json == INVALID_HANDLE)
+ {
+ LogError("Failed to get map info.");
+ return 0;
+ }
+
+ APIMap map = view_as<APIMap>(map_json);
+
+ gB_MapValidated = map.IsValidated;
+ gI_MapID = map.Id;
+ gI_MapFileSize = map.Filesize;
+ gI_MapTier = map.Difficulty;
+
+ // We don't do that earlier cause we need the map ID
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && IsClientAuthorized(client) && !IsFakeClient(client))
+ {
+ UpdatePoints(client);
+ }
+ }
+ return 0;
+}
+
+void CheckClientGlobalBan(int client)
+{
+ char steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ GlobalAPI_GetPlayerBySteamId(CheckClientGlobalBan_Callback, client, steamid);
+}
+
+public void CheckClientGlobalBan_Callback(JSON_Object player_json, GlobalAPIRequestData request, int client)
+{
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (request.Failure)
+ {
+ LogError("Failed to get ban info.");
+ return;
+ }
+
+ char client_steamid[32], response_steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, client_steamid, sizeof(client_steamid));
+
+ if (!player_json.IsArray || player_json.Length != 1)
+ {
+ LogError("Got malformed reply when querying steamid %s", client_steamid);
+ return;
+ }
+
+ APIPlayer player = view_as<APIPlayer>(player_json.GetObjectIndexed(0));
+ player.GetSteamId(response_steamid, sizeof(response_steamid));
+ if (!StrEqual(client_steamid, response_steamid))
+ {
+ return;
+ }
+
+ gB_GloballyVerified[client] = !player.IsBanned;
+
+ if (player.IsBanned && gB_GOKZLocalDB)
+ {
+ GOKZ_DB_SetCheater(client, true);
+ }
+}
+
+static void LoadSounds()
+{
+ char downloadPath[PLATFORM_MAX_PATH];
+ FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", GL_SOUND_NEW_RECORD);
+ AddFileToDownloadsTable(downloadPath);
+ PrecacheSound(GL_SOUND_NEW_RECORD, true);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-global/api.sp b/sourcemod/scripting/gokz-global/api.sp
new file mode 100644
index 0000000..23caa15
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/api.sp
@@ -0,0 +1,142 @@
+static GlobalForward H_OnNewTopTime;
+static GlobalForward H_OnPointsUpdated;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnNewTopTime = new GlobalForward("GOKZ_GL_OnNewTopTime", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float);
+ H_OnPointsUpdated = new GlobalForward("GOKZ_GL_OnPointsUpdated", ET_Ignore, Param_Cell, Param_Cell);
+}
+
+void Call_OnNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall, float time)
+{
+ Call_StartForward(H_OnNewTopTime);
+ Call_PushCell(client);
+ Call_PushCell(course);
+ Call_PushCell(mode);
+ Call_PushCell(timeType);
+ Call_PushCell(rank);
+ Call_PushCell(rankOverall);
+ Call_PushFloat(time);
+ Call_Finish();
+}
+
+void Call_OnPointsUpdated(int client, int mode)
+{
+ Call_StartForward(H_OnPointsUpdated);
+ Call_PushCell(client);
+ Call_PushCell(mode);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_GL_PrintRecords", Native_PrintRecords);
+ CreateNative("GOKZ_GL_DisplayMapTopMenu", Native_DisplayMapTopMenu);
+ CreateNative("GOKZ_GL_GetPoints", Native_GetPoints);
+ CreateNative("GOKZ_GL_GetMapPoints", Native_GetMapPoints);
+ CreateNative("GOKZ_GL_GetRankPoints", Native_GetRankPoints);
+ CreateNative("GOKZ_GL_GetFinishes", Native_GetFinishes);
+ CreateNative("GOKZ_GL_UpdatePoints", Native_UpdatePoints);
+ CreateNative("GOKZ_GL_GetAPIKeyValid", Native_GetAPIKeyValid);
+ CreateNative("GOKZ_GL_GetPluginsValid", Native_GetPluginsValid);
+ CreateNative("GOKZ_GL_GetSettingsEnforcerValid", Native_GetSettingsEnforcerValid);
+ CreateNative("GOKZ_GL_GetMapValid", Native_GetMapValid);
+ CreateNative("GOKZ_GL_GetPlayerValid", Native_GetPlayerValid);
+}
+
+public int Native_PrintRecords(Handle plugin, int numParams)
+{
+ char map[33], steamid[32];
+ GetNativeString(2, map, sizeof(map));
+ GetNativeString(5, steamid, sizeof(steamid));
+
+ if (StrEqual(map, ""))
+ {
+ PrintRecords(GetNativeCell(1), gC_CurrentMap, GetNativeCell(3), GetNativeCell(4), steamid);
+ }
+ else
+ {
+ PrintRecords(GetNativeCell(1), map, GetNativeCell(3), GetNativeCell(4), steamid);
+ }
+ return 0;
+}
+
+public int Native_DisplayMapTopMenu(Handle plugin, int numParams)
+{
+ char pluginName[32];
+ GetPluginFilename(plugin, pluginName, sizeof(pluginName));
+ bool localRanksCall = StrEqual(pluginName, "gokz-localranks.smx", false);
+
+ char map[33];
+ GetNativeString(2, map, sizeof(map));
+
+ if (StrEqual(map, ""))
+ {
+ DisplayMapTopSubmenu(GetNativeCell(1), gC_CurrentMap, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), localRanksCall);
+ }
+ else
+ {
+ DisplayMapTopSubmenu(GetNativeCell(1), map, GetNativeCell(3), GetNativeCell(4), GetNativeCell(5), localRanksCall);
+ }
+ return 0;
+}
+
+public int Native_GetPoints(Handle plugin, int numParams)
+{
+ return GetPoints(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3));
+}
+
+public int Native_GetMapPoints(Handle plugin, int numParams)
+{
+ return GetMapPoints(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3));
+}
+
+public int Native_GetRankPoints(Handle plugin, int numParams)
+{
+ return GetRankPoints(GetNativeCell(1), GetNativeCell(2));
+}
+
+public int Native_GetFinishes(Handle plugin, int numParams)
+{
+ return GetFinishes(GetNativeCell(1), GetNativeCell(2), GetNativeCell(3));
+}
+
+public int Native_UpdatePoints(Handle plugin, int numParams)
+{
+ // We're gonna always force an update here, cause otherwise the call doesn't really make sense
+ UpdatePoints(GetNativeCell(1), true, GetNativeCell(2));
+ return 0;
+}
+
+public int Native_GetAPIKeyValid(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_APIKeyCheck);
+}
+
+public int Native_GetPluginsValid(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_BannedCommandsCheck);
+}
+
+public int Native_GetSettingsEnforcerValid(Handle plugin, int numParams)
+{
+ return view_as<int>(gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap);
+}
+
+public int Native_GetMapValid(Handle plugin, int numParams)
+{
+ return view_as<int>(MapCheck());
+}
+
+public int Native_GetPlayerValid(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_GloballyVerified[GetNativeCell(1)]);
+}
diff --git a/sourcemod/scripting/gokz-global/ban_player.sp b/sourcemod/scripting/gokz-global/ban_player.sp
new file mode 100644
index 0000000..835d9e5
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/ban_player.sp
@@ -0,0 +1,42 @@
+/*
+ Globally ban players when they are suspected by gokz-anticheat.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+void GlobalBanPlayer(int client, ACReason reason, const char[] notes, const char[] stats)
+{
+ char playerName[MAX_NAME_LENGTH], steamid[32], ip[32];
+
+ GetClientName(client, playerName, sizeof(playerName));
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ GetClientIP(client, ip, sizeof(ip));
+
+ DataPack dp = new DataPack();
+ dp.WriteString(playerName);
+ dp.WriteString(steamid);
+
+ switch (reason)
+ {
+ case ACReason_BhopHack:GlobalAPI_CreateBan(BanPlayerCallback, dp, steamid, "bhop_hack", stats, notes, ip);
+ case ACReason_BhopMacro:GlobalAPI_CreateBan(BanPlayerCallback, dp, steamid, "bhop_macro", stats, notes, ip);
+ }
+}
+
+public int BanPlayerCallback(JSON_Object response, GlobalAPIRequestData request, DataPack dp)
+{
+ char playerName[MAX_NAME_LENGTH], steamid[32];
+
+ dp.Reset();
+ dp.ReadString(playerName, sizeof(playerName));
+ dp.ReadString(steamid, sizeof(steamid));
+ delete dp;
+
+ if (request.Failure)
+ {
+ LogError("Failed to globally ban %s (%s).", playerName, steamid);
+ }
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-global/commands.sp b/sourcemod/scripting/gokz-global/commands.sp
new file mode 100644
index 0000000..8ee7c32
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/commands.sp
@@ -0,0 +1,169 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_globalcheck", CommandGlobalCheck, "[KZ] Show whether global records are currently enabled in chat.");
+ RegConsoleCmd("sm_gc", CommandGlobalCheck, "[KZ] Show whether global records are currently enabled in chat.");
+ RegConsoleCmd("sm_tier", CommandTier, "[KZ] Show the map's tier in chat.");
+ RegConsoleCmd("sm_gpb", CommandPrintPBs, "[KZ] Show main course global personal best in chat. Usage: !gpb <map>");
+ RegConsoleCmd("sm_gr", CommandPrintRecords, "[KZ] Show main course global record times in chat. Usage: !gr <map>");
+ RegConsoleCmd("sm_gwr", CommandPrintRecords, "[KZ] Show main course global record times in chat. Usage: !gwr <map>");
+ RegConsoleCmd("sm_gbpb", CommandPrintBonusPBs, "[KZ] Show bonus global personal best in chat. Usage: !gbpb <#bonus> <map>");
+ RegConsoleCmd("sm_gbr", CommandPrintBonusRecords, "[KZ] Show bonus global record times in chat. Usage: !bgr <#bonus> <map>");
+ RegConsoleCmd("sm_gbwr", CommandPrintBonusRecords, "[KZ] Show bonus global record times in chat. Usage: !bgwr <#bonus> <map>");
+ RegConsoleCmd("sm_gmaptop", CommandMapTop, "[KZ] Open a menu showing the top global main course times of a map. Usage: !gmaptop <map>");
+ RegConsoleCmd("sm_gbmaptop", CommandBonusMapTop, "[KZ] Open a menu showing the top global bonus times of a map. Usage: !gbmaptop <#bonus> <map>");
+}
+
+public Action CommandGlobalCheck(int client, int args)
+{
+ PrintGlobalCheckToChat(client);
+ return Plugin_Handled;
+}
+
+public Action CommandTier(int client, int args)
+{
+ if (gI_MapTier != -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Tier", gC_CurrentMap, gI_MapTier);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Tier (Unknown)", gC_CurrentMap);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandPrintPBs(int client, int args)
+{
+ char steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ return CommandPrintRecordsHelper(client, args, steamid);
+}
+
+public Action CommandPrintRecords(int client, int args)
+{
+ return CommandPrintRecordsHelper(client, args);
+}
+
+static Action CommandPrintRecordsHelper(int client, int args, const char[] steamid = DEFAULT_STRING)
+{
+ KZPlayer player = KZPlayer(client);
+ int mode = player.Mode;
+
+ if (args == 0)
+ { // Print record times for current map and their current mode
+ PrintRecords(client, gC_CurrentMap, 0, mode, steamid);
+ }
+ else if (args >= 1)
+ { // Print record times for specified map and their current mode
+ char argMap[33];
+ GetCmdArg(1, argMap, sizeof(argMap));
+ PrintRecords(client, argMap, 0, mode, steamid);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandPrintBonusPBs(int client, int args)
+{
+ char steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ return CommandPrintBonusRecordsHelper(client, args, steamid);
+}
+
+public Action CommandPrintBonusRecords(int client, int args)
+{
+ return CommandPrintBonusRecordsHelper(client, args);
+}
+
+static Action CommandPrintBonusRecordsHelper(int client, int args, const char[] steamid = DEFAULT_STRING)
+{
+ KZPlayer player = KZPlayer(client);
+ int mode = player.Mode;
+
+ if (args == 0)
+ { // Print Bonus 1 record times for current map and their current mode
+ PrintRecords(client, gC_CurrentMap, 1, mode, steamid);
+ }
+ else if (args == 1)
+ { // Print specified Bonus # record times for current map and their current mode
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ PrintRecords(client, gC_CurrentMap, bonus, mode, steamid);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args >= 2)
+ { // Print specified Bonus # record times for specified map and their current mode
+ char argBonus[4], argMap[33];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ PrintRecords(client, argMap, bonus, mode, steamid);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandMapTop(int client, int args)
+{
+ if (args <= 0)
+ { // Open global map top for current map
+ DisplayMapTopModeMenu(client, gC_CurrentMap, 0);
+ }
+ else if (args >= 1)
+ { // Open global map top for specified map
+ char argMap[64];
+ GetCmdArg(1, argMap, sizeof(argMap));
+ DisplayMapTopModeMenu(client, argMap, 0);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandBonusMapTop(int client, int args)
+{
+ if (args == 0)
+ { // Open global Bonus 1 top for current map
+ DisplayMapTopModeMenu(client, gC_CurrentMap, 1);
+ }
+ else if (args == 1)
+ { // Open specified global Bonus # top for current map
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DisplayMapTopModeMenu(client, gC_CurrentMap, bonus);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args >= 2)
+ { // Open specified global Bonus # top for specified map
+ char argBonus[4], argMap[33];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DisplayMapTopModeMenu(client, argMap, bonus);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
diff --git a/sourcemod/scripting/gokz-global/maptop_menu.sp b/sourcemod/scripting/gokz-global/maptop_menu.sp
new file mode 100644
index 0000000..0c71346
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/maptop_menu.sp
@@ -0,0 +1,249 @@
+/*
+ Menu with the top global times for a map course and mode.
+*/
+
+static bool cameFromLocalRanks[MAXPLAYERS + 1];
+static char mapTopMap[MAXPLAYERS + 1][64];
+static int mapTopCourse[MAXPLAYERS + 1];
+static int mapTopMode[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayMapTopModeMenu(int client, const char[] map, int course)
+{
+ FormatEx(mapTopMap[client], sizeof(mapTopMap[]), map);
+ mapTopCourse[client] = course;
+
+ Menu menu = new Menu(MenuHandler_MapTopModeMenu);
+ MapTopModeMenuSetTitle(client, menu);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+void DisplayMapTopMenu(int client, const char[] map, int course, int mode)
+{
+ FormatEx(mapTopMap[client], sizeof(mapTopMap[]), map);
+ mapTopCourse[client] = course;
+ mapTopMode[client] = mode;
+
+ Menu menu = new Menu(MenuHandler_MapTopMenu);
+ MapTopMenuSetTitle(client, menu);
+ MapTopMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+void DisplayMapTopSubmenu(int client, const char[] map, int course, int mode, int timeType, bool fromLocalRanks = false)
+{
+ char modeStr[32];
+
+ cameFromLocalRanks[client] = fromLocalRanks;
+
+ DataPack dp = new DataPack();
+ dp.WriteCell(GetClientUserId(client));
+ dp.WriteCell(timeType);
+
+ FormatEx(mapTopMap[client], sizeof(mapTopMap[]), map);
+ mapTopCourse[client] = course;
+ mapTopMode[client] = mode;
+ GOKZ_GL_GetModeString(mode, modeStr, sizeof(modeStr));
+
+ // TODO Hard coded 128 tick
+ // TODO Hard coded cap at top 20
+ // TODO Not true NUB yet
+ GlobalAPI_GetRecordsTop(DisplayMapTopSubmenuCallback, dp, _, _, _, map, 128, course, modeStr,
+ timeType == TimeType_Nub ? DEFAULT_BOOL : false, _, 0, 20);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public int MenuHandler_MapTopModeMenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = mode
+ DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], param2);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_MapTopMenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[8];
+ menu.GetItem(param2, info, sizeof(info));
+ int timeType = StringToInt(info);
+ DisplayMapTopSubmenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1], timeType);
+ }
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayMapTopModeMenu(param1, mapTopMap[param1], mapTopCourse[param1]);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_MapTopSubmenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ if (cameFromLocalRanks[param1])
+ {
+ GOKZ_LR_ReopenMapTopMenu(param1);
+ }
+ else
+ {
+ DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1]);
+ }
+ }
+ if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void MapTopModeMenuSetTitle(int client, Menu menu)
+{
+ if (mapTopCourse[client] == 0)
+ {
+ menu.SetTitle("%T", "Global Map Top Mode Menu - Title", client, mapTopMap[client]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Global Map Top Mode Menu - Title (Bonus)", client, mapTopMap[client], mapTopCourse[client]);
+ }
+}
+
+static void MapTopMenuSetTitle(int client, Menu menu)
+{
+ if (mapTopCourse[client] == 0)
+ {
+ menu.SetTitle("%T", "Global Map Top Menu - Title", client, mapTopMap[client], gC_ModeNames[mapTopMode[client]]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Global Map Top Menu - Title (Bonus)", client, mapTopMap[client], mapTopCourse[client], gC_ModeNames[mapTopMode[client]]);
+ }
+}
+
+static void MapTopMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+ for (int i = 0; i < TIMETYPE_COUNT; i++)
+ {
+ FormatEx(display, sizeof(display), "%T", "Global Map Top Menu - Top", client, gC_TimeTypeNames[i]);
+ menu.AddItem(IntToStringEx(i), display);
+ }
+}
+
+public int DisplayMapTopSubmenuCallback(JSON_Object top, GlobalAPIRequestData request, DataPack dp)
+{
+ dp.Reset();
+ int client = GetClientOfUserId(dp.ReadCell());
+ int timeType = dp.ReadCell();
+ delete dp;
+
+ if (request.Failure)
+ {
+ LogError("Failed to get top records with Global API.");
+ return 0;
+ }
+
+ if (!top.IsArray)
+ {
+ LogError("GlobalAPI returned a malformed response while looking up the top records.");
+ return 0;
+ }
+
+ if (!IsValidClient(client))
+ {
+ return 0;
+ }
+
+ Menu menu = new Menu(MenuHandler_MapTopSubmenu);
+ if (mapTopCourse[client] == 0)
+ {
+ menu.SetTitle("%T", "Global Map Top Submenu - Title", client,
+ gC_TimeTypeNames[timeType], mapTopMap[client], gC_ModeNames[mapTopMode[client]]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Global Map Top Submenu - Title (Bonus)", client,
+ gC_TimeTypeNames[timeType], mapTopMap[client], mapTopCourse[client], gC_ModeNames[mapTopMode[client]]);
+ }
+
+ if (MapTopSubmenuAddItems(menu, top, timeType) == 0)
+ { // If no records found
+ if (timeType == TimeType_Pro)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Global Times Found (PRO)");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Global Times Found");
+ }
+
+ if (cameFromLocalRanks[client])
+ {
+ GOKZ_LR_ReopenMapTopMenu(client);
+ }
+ else
+ {
+ DisplayMapTopMenu(client, mapTopMap[client], mapTopCourse[client], mapTopMode[client]);
+ }
+ }
+ else
+ {
+ menu.Pagination = 5;
+ menu.Display(client, MENU_TIME_FOREVER);
+ }
+ return 0;
+}
+
+// Returns number of record times added to the menu
+static int MapTopSubmenuAddItems(Menu menu, JSON_Object records, int timeType)
+{
+ char playerName[MAX_NAME_LENGTH];
+ char display[128];
+
+ for (int i = 0; i < records.Length; i++)
+ {
+ APIRecord record = view_as<APIRecord>(records.GetObjectIndexed(i));
+
+ record.GetPlayerName(playerName, sizeof(playerName));
+
+ switch (timeType)
+ {
+ case TimeType_Nub:
+ {
+ FormatEx(display, sizeof(display), "#%-2d %11s %3d TP %s",
+ i + 1, GOKZ_FormatTime(record.Time), record.Teleports, playerName);
+ }
+ case TimeType_Pro:
+ {
+ FormatEx(display, sizeof(display), "#%-2d %11s %s",
+ i + 1, GOKZ_FormatTime(record.Time), playerName);
+ }
+ }
+
+ menu.AddItem("", display, ITEMDRAW_DISABLED);
+ }
+
+ return records.Length;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-global/points.sp b/sourcemod/scripting/gokz-global/points.sp
new file mode 100644
index 0000000..7758318
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/points.sp
@@ -0,0 +1,147 @@
+
+int pointsTotal[MAXPLAYERS + 1][MODE_COUNT][TIMETYPE_COUNT];
+int finishes[MAXPLAYERS + 1][MODE_COUNT][TIMETYPE_COUNT];
+int pointsMap[MAXPLAYERS + 1][MODE_COUNT][TIMETYPE_COUNT];
+int requestsInProgress[MAXPLAYERS + 1];
+
+
+
+void ResetPoints(int client)
+{
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ for (int type = 0; type < TIMETYPE_COUNT; type++)
+ {
+ pointsTotal[client][mode][type] = -1;
+ finishes[client][mode][type] = -1;
+ pointsMap[client][mode][type] = -1;
+ }
+ }
+ requestsInProgress[client] = 0;
+}
+
+void ResetMapPoints(int client)
+{
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ for (int type = 0; type < TIMETYPE_COUNT; type++)
+ {
+ pointsMap[client][mode][type] = -1;
+ }
+ }
+ requestsInProgress[client] = 0;
+}
+
+int GetRankPoints(int client, int mode)
+{
+ return pointsTotal[client][mode][TimeType_Nub];
+}
+
+int GetPoints(int client, int mode, int timeType)
+{
+ return pointsTotal[client][mode][timeType];
+}
+
+int GetMapPoints(int client, int mode, int timeType)
+{
+ return pointsMap[client][mode][timeType];
+}
+
+int GetFinishes(int client, int mode, int timeType)
+{
+ return finishes[client][mode][timeType];
+}
+
+// Note: This only gets 128 tick records
+void UpdatePoints(int client, bool force = false, int mode = -1)
+{
+ if (requestsInProgress[client] != 0)
+ {
+ return;
+ }
+
+ if (mode == -1)
+ {
+ mode = GOKZ_GetCoreOption(client, Option_Mode);
+ }
+
+ if (!force || pointsTotal[client][mode][TimeType_Nub] == -1)
+ {
+ GetPlayerRanks(client, mode, TimeType_Nub);
+ GetPlayerRanks(client, mode, TimeType_Pro);
+ requestsInProgress[client] += 2;
+ }
+
+ if (gI_MapID != -1 && (!force || pointsMap[client][mode][TimeType_Nub] == -1))
+ {
+ GetPlayerRanks(client, mode, TimeType_Nub, gI_MapID);
+ GetPlayerRanks(client, mode, TimeType_Pro, gI_MapID);
+ requestsInProgress[client] += 2;
+ }
+}
+
+static void GetPlayerRanks(int client, int mode, int timeType, int mapID = DEFAULT_INT)
+{
+ char steamid[21];
+ int modes[1], mapIDs[1];
+
+ modes[0] = view_as<int>(GOKZ_GL_GetGlobalMode(mode));
+ mapIDs[0] = mapID;
+ GetClientAuthId(client, AuthId_SteamID64, steamid, sizeof(steamid));
+
+ DataPack dp = new DataPack();
+ dp.WriteCell(GetClientUserId(client));
+ dp.WriteCell(mode);
+ dp.WriteCell(timeType);
+ dp.WriteCell(mapID == DEFAULT_INT);
+ GlobalAPI_GetPlayerRanks(UpdatePointsCallback, dp, _, _, _, _, steamid, _, _,
+ mapIDs, mapID == DEFAULT_INT ? DEFAULT_INT : 1, { 0 }, 1,
+ modes, 1, { 128 }, 1, timeType == TimeType_Nub ? DEFAULT_BOOL : false, _, _);
+}
+
+static void UpdatePointsCallback(JSON_Object ranks, GlobalAPIRequestData request, DataPack dp)
+{
+ dp.Reset();
+ int client = GetClientOfUserId(dp.ReadCell());
+ int mode = dp.ReadCell();
+ int timeType = dp.ReadCell();
+ bool isTotal = dp.ReadCell();
+ delete dp;
+
+ requestsInProgress[client]--;
+
+ if (client == 0)
+ {
+ return;
+ }
+
+ int points, totalFinishes;
+ if (request.Failure || !ranks.IsArray || ranks.Length == 0)
+ {
+ points = 0;
+ totalFinishes = 0;
+ }
+ else
+ {
+ APIPlayerRank rank = view_as<APIPlayerRank>(ranks.GetObjectIndexed(0));
+ // points = timeType == TimeType_Nub ? rank.PointsOverall : rank.Points;
+ points = points == -1 ? 0 : rank.Points;
+ totalFinishes = rank.Finishes == -1 ? 0 : rank.Finishes;
+ }
+
+ if (isTotal)
+ {
+ pointsTotal[client][mode][timeType] = points;
+ finishes[client][mode][timeType] = totalFinishes;
+ }
+ else
+ {
+ pointsMap[client][mode][timeType] = points;
+ }
+
+ // We always do that cause not all of the requests might have failed
+ if (requestsInProgress[client] == 0)
+ {
+ Call_OnPointsUpdated(client, mode);
+ }
+}
diff --git a/sourcemod/scripting/gokz-global/print_records.sp b/sourcemod/scripting/gokz-global/print_records.sp
new file mode 100644
index 0000000..3966ed5
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/print_records.sp
@@ -0,0 +1,190 @@
+/*
+ Prints the global record times for a map course and mode.
+*/
+
+
+static bool inProgress[MAXPLAYERS + 1];
+static bool waitingForOtherCallback[MAXPLAYERS + 1];
+static bool isPBQuery[MAXPLAYERS + 1];
+static char printRecordsMap[MAXPLAYERS + 1][64];
+static int printRecordsCourse[MAXPLAYERS + 1];
+static int printRecordsMode[MAXPLAYERS + 1];
+static bool printRecordsTimeExists[MAXPLAYERS + 1][TIMETYPE_COUNT];
+static float printRecordsTimes[MAXPLAYERS + 1][TIMETYPE_COUNT];
+static char printRecordsPlayerNames[MAXPLAYERS + 1][TIMETYPE_COUNT][MAX_NAME_LENGTH];
+
+
+
+// =====[ PUBLIC ]=====
+
+void PrintRecords(int client, const char[] map, int course, int mode, const char[] steamid = DEFAULT_STRING)
+{
+ char mode_str[32];
+
+ if (inProgress[client])
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Please Wait Before Using Command Again");
+ return;
+ }
+
+ GOKZ_GL_GetModeString(mode, mode_str, sizeof(mode_str));
+
+ DataPack dpNUB = CreateDataPack();
+ dpNUB.WriteCell(GetClientUserId(client));
+ dpNUB.WriteCell(TimeType_Nub);
+ GlobalAPI_GetRecordsTop(PrintRecordsCallback, dpNUB, steamid, _, _, map, 128, course, mode_str, _, _, 0, 1);
+
+ DataPack dpPRO = CreateDataPack();
+ dpPRO.WriteCell(GetClientUserId(client));
+ dpPRO.WriteCell(TimeType_Pro);
+ GlobalAPI_GetRecordsTop(PrintRecordsCallback, dpPRO, steamid, _, _, map, 128, course, mode_str, false, _, 0, 1);
+
+ inProgress[client] = true;
+ waitingForOtherCallback[client] = true;
+ isPBQuery[client] = !StrEqual(steamid, DEFAULT_STRING);
+ FormatEx(printRecordsMap[client], sizeof(printRecordsMap[]), map);
+ printRecordsCourse[client] = course;
+ printRecordsMode[client] = mode;
+}
+
+public int PrintRecordsCallback(JSON_Object records, GlobalAPIRequestData request, DataPack dp)
+{
+ dp.Reset();
+ int client = GetClientOfUserId(dp.ReadCell());
+ int timeType = dp.ReadCell();
+ delete dp;
+
+ if (request.Failure)
+ {
+ LogError("Failed to retrieve record from the Global API for printing.");
+ return 0;
+ }
+
+ if (!IsValidClient(client))
+ {
+ return 0;
+ }
+
+ if (records.Length <= 0)
+ {
+ printRecordsTimeExists[client][timeType] = false;
+ }
+ else
+ {
+ APIRecord record = view_as<APIRecord>(records.GetObjectIndexed(0));
+ printRecordsTimeExists[client][timeType] = true;
+ printRecordsTimes[client][timeType] = record.Time;
+ record.GetPlayerName(printRecordsPlayerNames[client][timeType], sizeof(printRecordsPlayerNames[][]));
+ }
+
+ if (!waitingForOtherCallback[client])
+ {
+ if (isPBQuery[client])
+ {
+ PrintPBsFinally(client);
+ }
+ else
+ {
+ PrintRecordsFinally(client);
+ }
+ inProgress[client] = false;
+ }
+ else
+ {
+ waitingForOtherCallback[client] = false;
+ }
+ return 0;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_PrintRecords(int client)
+{
+ inProgress[client] = false;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void PrintPBsFinally(int client)
+{
+ // Print GPB header to chat
+ if (printRecordsCourse[client] == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "GPB Header",
+ printRecordsMap[client],
+ gC_ModeNamesShort[printRecordsMode[client]]);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "GPB Header (Bonus)",
+ printRecordsMap[client],
+ printRecordsCourse[client],
+ gC_ModeNamesShort[printRecordsMode[client]]);
+ }
+
+ // Print GPB times to chat
+ if (!printRecordsTimeExists[client][TimeType_Nub])
+ {
+ GOKZ_PrintToChat(client, false, "%t", "No Global Times Found");
+ }
+ else if (!printRecordsTimeExists[client][TimeType_Pro])
+ {
+ GOKZ_PrintToChat(client, false, "%t", "GPB Time - NUB",
+ GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]),
+ printRecordsPlayerNames[client][TimeType_Nub]);
+ GOKZ_PrintToChat(client, false, "%t", "GPB Time - No PRO Time");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, false, "%t", "GPB Time - NUB",
+ GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]),
+ printRecordsPlayerNames[client][TimeType_Nub]);
+ GOKZ_PrintToChat(client, false, "%t", "GPB Time - PRO",
+ GOKZ_FormatTime(printRecordsTimes[client][TimeType_Pro]),
+ printRecordsPlayerNames[client][TimeType_Pro]);
+ }
+}
+
+static void PrintRecordsFinally(int client)
+{
+ // Print GWR header to chat
+ if (printRecordsCourse[client] == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "GWR Header",
+ printRecordsMap[client],
+ gC_ModeNamesShort[printRecordsMode[client]]);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "GWR Header (Bonus)",
+ printRecordsMap[client],
+ printRecordsCourse[client],
+ gC_ModeNamesShort[printRecordsMode[client]]);
+ }
+
+ // Print GWR times to chat
+ if (!printRecordsTimeExists[client][TimeType_Nub])
+ {
+ GOKZ_PrintToChat(client, false, "%t", "No Global Times Found");
+ }
+ else if (!printRecordsTimeExists[client][TimeType_Pro])
+ {
+ GOKZ_PrintToChat(client, false, "%t", "GWR Time - NUB",
+ GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]),
+ printRecordsPlayerNames[client][TimeType_Nub]);
+ GOKZ_PrintToChat(client, false, "%t", "GWR Time - No PRO Time");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, false, "%t", "GWR Time - NUB",
+ GOKZ_FormatTime(printRecordsTimes[client][TimeType_Nub]),
+ printRecordsPlayerNames[client][TimeType_Nub]);
+ GOKZ_PrintToChat(client, false, "%t", "GWR Time - PRO",
+ GOKZ_FormatTime(printRecordsTimes[client][TimeType_Pro]),
+ printRecordsPlayerNames[client][TimeType_Pro]);
+ }
+}
diff --git a/sourcemod/scripting/gokz-global/send_run.sp b/sourcemod/scripting/gokz-global/send_run.sp
new file mode 100644
index 0000000..f560958
--- /dev/null
+++ b/sourcemod/scripting/gokz-global/send_run.sp
@@ -0,0 +1,143 @@
+/*
+ Sends a run to the global API and delete the replay if it is a temporary replay.
+*/
+
+
+
+char storedReplayPath[MAXPLAYERS + 1][512];
+int lastRecordId[MAXPLAYERS + 1], storedCourse[MAXPLAYERS + 1], storedTimeType[MAXPLAYERS + 1], storedUserId[MAXPLAYERS + 1];
+float storedTime[MAXPLAYERS + 1];
+bool deleteRecord[MAXPLAYERS + 1];
+
+// =====[ PUBLIC ]=====
+
+void SendTime(int client, int course, float time, int teleportsUsed)
+{
+ char steamid[32], modeStr[32];
+ KZPlayer player = KZPlayer(client);
+ int mode = player.Mode;
+
+ if (GlobalsEnabled(mode))
+ {
+ DataPack dp = CreateDataPack();
+ dp.WriteCell(GetClientUserId(client));
+ dp.WriteCell(course);
+ dp.WriteCell(mode);
+ dp.WriteCell(GOKZ_GetTimeTypeEx(teleportsUsed));
+ dp.WriteFloat(time);
+
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ GOKZ_GL_GetModeString(mode, modeStr, sizeof(modeStr));
+ GlobalAPI_CreateRecord(SendTimeCallback, dp, steamid, gI_MapID, modeStr, course, 128, teleportsUsed, time);
+ }
+}
+
+public int SendTimeCallback(JSON_Object response, GlobalAPIRequestData request, DataPack dp)
+{
+ dp.Reset();
+ int userID = dp.ReadCell();
+ int client = GetClientOfUserId(userID);
+ int course = dp.ReadCell();
+ int mode = dp.ReadCell();
+ int timeType = dp.ReadCell();
+ float time = dp.ReadFloat();
+ delete dp;
+
+ if (!IsValidClient(client))
+ {
+ return 0;
+ }
+
+ if (request.Failure)
+ {
+ LogError("Failed to send a time to the global API.");
+ return 0;
+ }
+
+ int top_place = response.GetInt("top_100");
+ int top_overall_place = response.GetInt("top_100_overall");
+
+ if (top_place > 0)
+ {
+ Call_OnNewTopTime(client, course, mode, timeType, top_place, top_overall_place, time);
+ }
+
+ // Don't like doing this here, but seems to be the most efficient place
+ UpdatePoints(client, true);
+
+ // Check if we can send the replay
+ lastRecordId[client] = response.GetInt("record_id");
+ if (IsReplayReadyToSend(client, course, timeType, time))
+ {
+ SendReplay(client);
+ }
+ else
+ {
+ storedUserId[client] = userID;
+ storedCourse[client] = course;
+ storedTimeType[client] = timeType;
+ storedTime[client] = time;
+ }
+ return 0;
+}
+
+public void OnReplaySaved_SendReplay(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay)
+{
+ strcopy(storedReplayPath[client], sizeof(storedReplayPath[]), filePath);
+ if (IsReplayReadyToSend(client, course, timeType, time))
+ {
+ SendReplay(client);
+ }
+ else
+ {
+ lastRecordId[client] = -1;
+ storedUserId[client] = GetClientUserId(client);
+ storedCourse[client] = course;
+ storedTimeType[client] = timeType;
+ storedTime[client] = time;
+ deleteRecord[client] = tempReplay;
+ }
+}
+
+bool IsReplayReadyToSend(int client, int course, int timeType, float time)
+{
+ // Not an error, just not ready yet
+ if (lastRecordId[client] == -1 || storedReplayPath[client][0] == '\0')
+ {
+ return false;
+ }
+
+ if (storedUserId[client] == GetClientUserId(client) && storedCourse[client] == course
+ && storedTimeType[client] == timeType && storedTime[client] == time)
+ {
+ return true;
+ }
+ else
+ {
+ LogError("Failed to upload replay to the global API. Record mismatch.");
+ return false;
+ }
+}
+
+public void SendReplay(int client)
+{
+ DataPack dp = new DataPack();
+ dp.WriteString(deleteRecord[client] ? storedReplayPath[client] : "");
+ GlobalAPI_CreateReplayForRecordId(SendReplayCallback, dp, lastRecordId[client], storedReplayPath[client]);
+ lastRecordId[client] = -1;
+ storedReplayPath[client] = "";
+}
+
+public int SendReplayCallback(JSON_Object response, GlobalAPIRequestData request, DataPack dp)
+{
+ // Delete the temporary replay file if needed.
+ dp.Reset();
+ char replayPath[PLATFORM_MAX_PATH];
+ dp.ReadString(replayPath, sizeof(replayPath));
+ if (replayPath[0] != '\0')
+ {
+ DeleteFile(replayPath);
+ }
+ delete dp;
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-goto.sp b/sourcemod/scripting/gokz-goto.sp
new file mode 100644
index 0000000..7dd67cb
--- /dev/null
+++ b/sourcemod/scripting/gokz-goto.sp
@@ -0,0 +1,231 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdktools>
+
+#include <gokz/core>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Goto",
+ author = "DanZay",
+ description = "Allows players to teleport to another player",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-goto.txt"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-goto");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("common.phrases");
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-goto.phrases");
+
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ GOTO ]=====
+
+// Returns whether teleport to target was successful
+bool GotoPlayer(int client, int target, bool printMessage = true)
+{
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ if (printMessage)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (target == client)
+ {
+ if (printMessage)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Goto Failure (Not Yourself)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ if (!IsPlayerAlive(target))
+ {
+ if (printMessage)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Goto Failure (Dead)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+
+ float targetOrigin[3];
+ float targetAngles[3];
+
+ Movement_GetOrigin(target, targetOrigin);
+ Movement_GetEyeAngles(target, targetAngles);
+
+ if (!IsPlayerAlive(client))
+ {
+ GOKZ_RespawnPlayer(client);
+ }
+
+ TeleportPlayer(client, targetOrigin, targetAngles);
+
+ GOKZ_PrintToChat(client, true, "%t", "Goto Success", target);
+
+ if (GOKZ_GetTimerRunning(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Goto)");
+ GOKZ_StopTimer(client);
+ }
+
+ return true;
+}
+
+
+
+// =====[ GOTO MENU ]=====
+
+int DisplayGotoMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_Goto);
+ menu.SetTitle("%T", "Goto Menu - Title", client);
+ int menuItems = GotoMenuAddItems(client, menu);
+ if (menuItems == 0)
+ {
+ delete menu;
+ }
+ else
+ {
+ menu.Display(client, MENU_TIME_FOREVER);
+ }
+ return menuItems;
+}
+
+public int MenuHandler_Goto(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+ int target = GetClientOfUserId(StringToInt(info));
+
+ if (!IsValidClient(target))
+ {
+ GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid");
+ GOKZ_PlayErrorSound(param1);
+ DisplayGotoMenu(param1);
+ }
+ else if (!GotoPlayer(param1, target))
+ {
+ DisplayGotoMenu(param1);
+ }
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+// Returns number of items added to the menu
+int GotoMenuAddItems(int client, Menu menu)
+{
+ char display[MAX_NAME_LENGTH + 4];
+ int targetCount = 0;
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (!IsClientInGame(i) || !IsPlayerAlive(i) || i == client)
+ {
+ continue;
+ }
+
+ if (IsFakeClient(i))
+ {
+ FormatEx(display, sizeof(display), "BOT %N", i);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%N", i);
+ }
+
+ menu.AddItem(IntToStringEx(GetClientUserId(i)), display, ITEMDRAW_DEFAULT);
+ targetCount++;
+ }
+
+ return targetCount;
+}
+
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_goto", CommandGoto, "[KZ] Teleport to another player. Usage: !goto <player>");
+}
+
+public Action CommandGoto(int client, int args)
+{
+ // If no arguments, display the goto menu
+ if (args < 1)
+ {
+ if (DisplayGotoMenu(client) == 0)
+ {
+ // No targets, so show error
+ GOKZ_PrintToChat(client, true, "%t", "No Players Found");
+ GOKZ_PlayErrorSound(client);
+ }
+ }
+ // Otherwise try to teleport to the specified player
+ else
+ {
+ char specifiedPlayer[MAX_NAME_LENGTH];
+ GetCmdArg(1, specifiedPlayer, sizeof(specifiedPlayer));
+
+ int target = FindTarget(client, specifiedPlayer, false, false);
+ if (target != -1)
+ {
+ GotoPlayer(client, target);
+ }
+ }
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud.sp b/sourcemod/scripting/gokz-hud.sp
new file mode 100644
index 0000000..e9db12a
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud.sp
@@ -0,0 +1,334 @@
+#include <sourcemod>
+
+#include <sdkhooks>
+#include <gokz/core>
+#include <gokz/hud>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/racing>
+#include <gokz/replays>
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ HUD",
+ author = "DanZay",
+ description = "Provides HUD and UI features",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-hud.txt"
+
+bool gB_GOKZRacing;
+bool gB_GOKZReplays;
+bool gB_MenuShowing[MAXPLAYERS + 1];
+int gI_ObserverTarget[MAXPLAYERS + 1];
+bool gB_JBTakeoff[MAXPLAYERS + 1];
+bool gB_FastUpdateRate[MAXPLAYERS + 1];
+int gI_DynamicMenu[MAXPLAYERS + 1];
+
+#include "gokz-hud/spectate_text.sp"
+#include "gokz-hud/commands.sp"
+#include "gokz-hud/hide_weapon.sp"
+#include "gokz-hud/info_panel.sp"
+#include "gokz-hud/menu.sp"
+#include "gokz-hud/options.sp"
+#include "gokz-hud/options_menu.sp"
+#include "gokz-hud/racing_text.sp"
+#include "gokz-hud/speed_text.sp"
+#include "gokz-hud/timer_text.sp"
+#include "gokz-hud/tp_menu.sp"
+#include "gokz-hud/natives.sp"
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-hud");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-hud.phrases");
+
+ HookEvents();
+ RegisterCommands();
+
+ UpdateSpecList();
+ OnPluginStart_RacingText();
+ OnPluginStart_SpeedText();
+ OnPluginStart_TimerText();
+}
+
+public void OnPluginEnd()
+{
+ OnPluginEnd_Menu();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+
+ gB_GOKZRacing = LibraryExists("gokz-racing");
+ gB_GOKZReplays = LibraryExists("gokz-replays");
+ 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_GOKZRacing = gB_GOKZRacing || StrEqual(name, "gokz-racing");
+ gB_GOKZReplays = gB_GOKZReplays || StrEqual(name, "gokz-replays");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZRacing = gB_GOKZRacing && !StrEqual(name, "gokz-racing");
+ gB_GOKZReplays = gB_GOKZReplays && !StrEqual(name, "gokz-replays");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientDisconnect(int client)
+{
+ gI_ObserverTarget[client] = -1;
+}
+
+public void OnClientPutInServer(int client)
+{
+ SDKHook(client, SDKHook_PostThinkPost, OnPlayerPostThinkPost);
+}
+
+public void OnPlayerPostThinkPost(int client)
+{
+ KZPlayer player = KZPlayer(client);
+ gB_JBTakeoff[client] = (gB_JBTakeoff[client] && !player.OnGround && !player.OnLadder && !player.Noclipping) || Movement_GetJumpbugged(client);
+}
+
+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;
+ }
+
+ HUDInfo info;
+ KZPlayer player = KZPlayer(client);
+ KZPlayer targetPlayer = KZPlayer(player.ObserverTarget);
+
+ // Bots don't need to have their HUD drawn
+ if (player.Fake)
+ {
+ return;
+ }
+
+ if (player.Alive)
+ {
+ SetHUDInfo(player, info, cmdnum);
+ }
+ else if (targetPlayer.ID != -1 && !targetPlayer.Fake)
+ {
+ SetHUDInfo(targetPlayer, info, cmdnum);
+ }
+ else if (targetPlayer.ID != -1 && gB_GOKZReplays)
+ {
+ GOKZ_RP_GetPlaybackInfo(targetPlayer.ID, info);
+ }
+ else
+ {
+ return;
+ }
+
+ if (!IsValidClient(info.ID))
+ {
+ return;
+ }
+
+ OnPlayerRunCmdPost_InfoPanel(client, cmdnum, info);
+ OnPlayerRunCmdPost_RacingText(client, cmdnum);
+ OnPlayerRunCmdPost_SpeedText(client, cmdnum, info);
+ OnPlayerRunCmdPost_TimerText(client, cmdnum, info);
+ OnPlayerRunCmdPost_TPMenu(client, cmdnum, info);
+}
+
+public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ OnPlayerSpawn_HideWeapon(client);
+ OnPlayerSpawn_Menu(client);
+ }
+}
+
+public Action OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) // player_death pre hook
+{
+ event.BroadcastDisabled = true; // Block death notices
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnJoinTeam(int client, int team)
+{
+ OnJoinTeam_Menu(client);
+}
+
+public void GOKZ_OnTimerStart_Post(int client, int course)
+{
+ OnTimerStart_Menu(client);
+}
+
+public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ OnTimerEnd_TimerText(client);
+ OnTimerEnd_Menu(client);
+}
+
+public void GOKZ_OnTimerStopped(int client)
+{
+ OnTimerStopped_TimerText(client);
+ OnTimerStopped_Menu(client);
+}
+
+public void GOKZ_OnPause_Post(int client)
+{
+ OnPause_Menu(client);
+}
+
+public void GOKZ_OnResume_Post(int client)
+{
+ OnResume_Menu(client);
+}
+
+public void GOKZ_OnMakeCheckpoint_Post(int client)
+{
+ OnMakeCheckpoint_Menu(client);
+}
+
+public void GOKZ_OnCountedTeleport_Post(int client)
+{
+ OnCountedTeleport_Menu(client);
+}
+
+public void GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3])
+{
+ OnStartPositionSet_Menu(client);
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ any hudOption;
+ if (GOKZ_HUD_IsHUDOption(option, hudOption))
+ {
+ OnOptionChanged_SpeedText(client, hudOption);
+ OnOptionChanged_TimerText(client, hudOption);
+ OnOptionChanged_Menu(client, hudOption);
+ OnOptionChanged_HideWeapon(client, hudOption);
+ OnOptionChanged_Options(client, hudOption, newValue);
+ if (hudOption == HUDOption_UpdateRate)
+ {
+ gB_FastUpdateRate[client] = GOKZ_HUD_GetOption(client, HUDOption_UpdateRate) == UpdateRate_Fast;
+ }
+ else if (hudOption == HUDOption_DynamicMenu)
+ {
+ gI_DynamicMenu[client] = GOKZ_HUD_GetOption(client, HUDOption_DynamicMenu);
+ }
+ }
+}
+
+public void GOKZ_OnOptionsLoaded(int client)
+{
+ gB_FastUpdateRate[client] = GOKZ_HUD_GetOption(client, HUDOption_UpdateRate) == UpdateRate_Fast;
+ gI_DynamicMenu[client] = GOKZ_HUD_GetOption(client, HUDOption_DynamicMenu);
+}
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnGameFrame()
+{
+ // Cache the spectator list every few ticks.
+ if (GetGameTickCount() % 4 == 0)
+ {
+ UpdateSpecList();
+ }
+}
+
+public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ OnOptionsMenuCreated_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+public void GOKZ_RC_OnRaceInfoChanged(int raceID, RaceInfo prop, int oldValue, int newValue)
+{
+ OnRaceInfoChanged_RacingText(raceID, prop, newValue);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void HookEvents()
+{
+ HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post);
+ HookEvent("player_death", OnPlayerDeath, EventHookMode_Pre);
+}
+
+static void SetHUDInfo(KZPlayer player, HUDInfo info, int cmdnum)
+{
+ info.TimerRunning = player.TimerRunning;
+ info.TimeType = player.TimeType;
+ info.Time = player.Time;
+ info.Paused = player.Paused;
+ info.OnGround = player.OnGround;
+ info.OnLadder = player.OnLadder;
+ info.Noclipping = player.Noclipping;
+ info.Ducking = Movement_GetDucking(player.ID);
+ info.HitBhop = (Movement_GetJumped(player.ID) && Movement_GetTakeoffCmdNum(player.ID) == cmdnum) && Movement_GetTakeoffCmdNum(player.ID) - Movement_GetLandingCmdNum(player.ID) <= HUD_MAX_BHOP_GROUND_TICKS;
+ info.Speed = player.Speed;
+ info.ID = player.ID;
+ info.Jumped = player.Jumped;
+ info.HitPerf = player.GOKZHitPerf;
+ info.HitJB = gB_JBTakeoff[info.ID];
+ info.TakeoffSpeed = player.GOKZTakeoffSpeed;
+ info.IsTakeoff = Movement_GetTakeoffCmdNum(player.ID) == cmdnum;
+ info.Buttons = player.Buttons;
+ info.CurrentTeleport = player.TeleportCount;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/commands.sp b/sourcemod/scripting/gokz-hud/commands.sp
new file mode 100644
index 0000000..fc5ed4e
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/commands.sp
@@ -0,0 +1,116 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_menu", CommandMenu, "[KZ] Toggle the simple teleport menu.");
+ RegConsoleCmd("sm_cpmenu", CommandMenu, "[KZ] Toggle the simple teleport menu.");
+ RegConsoleCmd("sm_adv", CommandToggleAdvancedMenu, "[KZ] Toggle the advanced teleport menu.");
+ RegConsoleCmd("sm_panel", CommandToggleInfoPanel, "[KZ] Toggle visibility of the centre information panel.");
+ RegConsoleCmd("sm_timerstyle", CommandToggleTimerStyle, "[KZ] Toggle the style of the timer text.");
+ RegConsoleCmd("sm_timertype", CommandToggleTimerType, "[KZ] Toggle visibility of your time type.");
+ RegConsoleCmd("sm_speed", CommandToggleSpeed, "[KZ] Toggle visibility of your speed and jump pre-speed.");
+ RegConsoleCmd("sm_hideweapon", CommandToggleShowWeapon, "[KZ] Toggle visibility of your weapon.");
+}
+
+public Action CommandMenu(int client, int args)
+{
+ if (GOKZ_HUD_GetOption(client, HUDOption_TPMenu) != TPMenu_Disabled)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Disabled);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Simple);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleAdvancedMenu(int client, int args)
+{
+ if (GOKZ_HUD_GetOption(client, HUDOption_TPMenu) != TPMenu_Advanced)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Advanced);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TPMenu, TPMenu_Simple);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleInfoPanel(int client, int args)
+{
+ if (GOKZ_HUD_GetOption(client, HUDOption_InfoPanel) == InfoPanel_Disabled)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_InfoPanel, InfoPanel_Enabled);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_InfoPanel, InfoPanel_Disabled);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleTimerStyle(int client, int args)
+{
+ if (GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Standard)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TimerStyle, TimerStyle_Precise);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TimerStyle, TimerStyle_Standard);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleTimerType(int client, int args)
+{
+ if (GOKZ_HUD_GetOption(client, HUDOption_TimerType) == TimerType_Disabled)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TimerType, TimerType_Enabled);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_TimerType, TimerType_Disabled);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleSpeed(int client, int args)
+{
+ int speedText = GOKZ_HUD_GetOption(client, HUDOption_SpeedText);
+ int infoPanel = GOKZ_HUD_GetOption(client, HUDOption_InfoPanel);
+
+ if (speedText == SpeedText_Disabled)
+ {
+ if (infoPanel == InfoPanel_Enabled)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_InfoPanel);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_Bottom);
+ }
+ }
+ else if (infoPanel == InfoPanel_Disabled && speedText == SpeedText_InfoPanel)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_Bottom);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_SpeedText, SpeedText_Disabled);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandToggleShowWeapon(int client, int args)
+{
+ if (GOKZ_HUD_GetOption(client, HUDOption_ShowWeapon) == ShowWeapon_Disabled)
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_ShowWeapon, ShowWeapon_Enabled);
+ }
+ else
+ {
+ GOKZ_HUD_SetOption(client, HUDOption_ShowWeapon, ShowWeapon_Disabled);
+ }
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/hide_weapon.sp b/sourcemod/scripting/gokz-hud/hide_weapon.sp
new file mode 100644
index 0000000..b290d8f
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/hide_weapon.sp
@@ -0,0 +1,30 @@
+/*
+ Hides weapon view model.
+*/
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPlayerSpawn_HideWeapon(int client)
+{
+ UpdateHideWeapon(client);
+}
+
+void OnOptionChanged_HideWeapon(int client, HUDOption option)
+{
+ if (option == HUDOption_ShowWeapon)
+ {
+ UpdateHideWeapon(client);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void UpdateHideWeapon(int client)
+{
+ SetEntProp(client, Prop_Send, "m_bDrawViewmodel",
+ GOKZ_HUD_GetOption(client, HUDOption_ShowWeapon) == ShowWeapon_Enabled);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/info_panel.sp b/sourcemod/scripting/gokz-hud/info_panel.sp
new file mode 100644
index 0000000..3563404
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/info_panel.sp
@@ -0,0 +1,307 @@
+/*
+ Displays information using hint text.
+
+ This is manually refreshed whenever player has taken off so that they see
+ their pre-speed as soon as possible, improving responsiveness.
+*/
+
+
+
+static bool infoPanelDuckPressedLast[MAXPLAYERS + 1];
+static bool infoPanelOnGroundLast[MAXPLAYERS + 1];
+static bool infoPanelShowDuckString[MAXPLAYERS + 1];
+
+
+// =====[ PUBLIC ]=====
+
+bool IsDrawingInfoPanel(int client)
+{
+ KZPlayer player = KZPlayer(client);
+ return player.InfoPanel != InfoPanel_Disabled
+ && !NothingEnabledInInfoPanel(player);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPlayerRunCmdPost_InfoPanel(int client, int cmdnum, HUDInfo info)
+{
+ int updateSpeed = gB_FastUpdateRate[client] ? 1 : 10;
+ if (cmdnum % updateSpeed == 0 || info.IsTakeoff)
+ {
+ UpdateInfoPanel(client, info);
+ }
+ infoPanelOnGroundLast[info.ID] = info.OnGround;
+ infoPanelDuckPressedLast[info.ID] = info.Ducking;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void UpdateInfoPanel(int client, HUDInfo info)
+{
+ KZPlayer player = KZPlayer(client);
+
+ if (player.Fake || !IsDrawingInfoPanel(player.ID))
+ {
+ return;
+ }
+ char infoPanelText[512];
+ FormatEx(infoPanelText, sizeof(infoPanelText), GetInfoPanel(player, info));
+ if (infoPanelText[0] != '\0')
+ {
+ PrintCSGOHUDText(player.ID, infoPanelText);
+ }
+}
+
+static bool NothingEnabledInInfoPanel(KZPlayer player)
+{
+ bool noTimerText = player.TimerText != TimerText_InfoPanel;
+ bool noSpeedText = player.SpeedText != SpeedText_InfoPanel || player.Paused;
+ bool noKeys = player.ShowKeys == ShowKeys_Disabled
+ || player.ShowKeys == ShowKeys_Spectating && player.Alive;
+ return noTimerText && noSpeedText && noKeys;
+}
+
+static char[] GetInfoPanel(KZPlayer player, HUDInfo info)
+{
+ char infoPanelText[512];
+ FormatEx(infoPanelText, sizeof(infoPanelText),
+ "%s%s%s%s",
+ GetSpectatorString(player, info),
+ GetTimeString(player, info),
+ GetSpeedString(player, info),
+ GetKeysString(player, info));
+ if (infoPanelText[0] == '\0')
+ {
+ return infoPanelText;
+ }
+ else
+ {
+ Format(infoPanelText, sizeof(infoPanelText), "<font color='#ffffff08'>%s", infoPanelText);
+ }
+ TrimString(infoPanelText);
+ return infoPanelText;
+}
+
+static char[] GetSpectatorString(KZPlayer player, HUDInfo info)
+{
+ char spectatorString[255];
+ if (player.SpecListPosition != SpecListPosition_InfoPanel || player.ShowSpectators == ShowSpecs_Disabled)
+ {
+ return spectatorString;
+ }
+ // Only return something if the player is alive or observing someone else
+ if (player.Alive || player.ObserverTarget != -1)
+ {
+ FormatEx(spectatorString, sizeof(spectatorString), "%s", FormatSpectatorTextForInfoPanel(player, KZPlayer(info.ID)));
+ }
+ return spectatorString;
+}
+
+static char[] GetTimeString(KZPlayer player, HUDInfo info)
+{
+ char timeString[128];
+ if (player.TimerText != TimerText_InfoPanel)
+ {
+ timeString = "";
+ }
+ else if (info.TimerRunning)
+ {
+ if (player.GetHUDOption(HUDOption_TimerType) == TimerType_Enabled)
+ {
+ switch (info.TimeType)
+ {
+ case TimeType_Nub:
+ {
+ FormatEx(timeString, sizeof(timeString),
+ "%T: <font color='#ead18a'>%s</font> %s\n",
+ "Info Panel Text - Time", player.ID,
+ GOKZ_HUD_FormatTime(player.ID, info.Time),
+ GetPausedString(player, info));
+ }
+ case TimeType_Pro:
+ {
+ FormatEx(timeString, sizeof(timeString),
+ "%T: <font color='#b5d4ee'>%s</font> %s\n",
+ "Info Panel Text - Time", player.ID,
+ GOKZ_HUD_FormatTime(player.ID, info.Time),
+ GetPausedString(player, info));
+ }
+ }
+ }
+ else
+ {
+ FormatEx(timeString, sizeof(timeString),
+ "%T: <font color='#ffffff'>%s</font> %s\n",
+ "Info Panel Text - Time", player.ID,
+ GOKZ_HUD_FormatTime(player.ID, info.Time),
+ GetPausedString(player, info));
+ }
+ }
+ else
+ {
+ FormatEx(timeString, sizeof(timeString),
+ "%T: <font color='#ea4141'>%T</font> %s\n",
+ "Info Panel Text - Time", player.ID,
+ "Info Panel Text - Stopped", player.ID,
+ GetPausedString(player, info));
+ }
+ return timeString;
+}
+
+static char[] GetPausedString(KZPlayer player, HUDInfo info)
+{
+ char pausedString[64];
+ if (info.Paused)
+ {
+ FormatEx(pausedString, sizeof(pausedString),
+ "(<font color='#ffffff'>%T</font>)",
+ "Info Panel Text - PAUSED", player.ID);
+ }
+ else
+ {
+ pausedString = "";
+ }
+ return pausedString;
+}
+
+static char[] GetSpeedString(KZPlayer player, HUDInfo info)
+{
+ char speedString[128];
+ if (player.SpeedText != SpeedText_InfoPanel || info.Paused)
+ {
+ speedString = "";
+ }
+ else
+ {
+ if (info.OnGround || info.OnLadder || info.Noclipping)
+ {
+ FormatEx(speedString, sizeof(speedString),
+ "%T: <font color='#ffffff'>%.0f</font> u/s\n",
+ "Info Panel Text - Speed", player.ID,
+ RoundToPowerOfTen(info.Speed, -2));
+ infoPanelShowDuckString[info.ID] = false;
+ }
+ else
+ {
+ if (GOKZ_HUD_GetOption(player.ID, HUDOption_DeadstrafeColor) == DeadstrafeColor_Enabled
+ && Movement_GetVerticalVelocity(info.ID) > 0.0 && Movement_GetVerticalVelocity(info.ID) < 140.0)
+ {
+ FormatEx(speedString, sizeof(speedString),
+ "%T: <font color='#ff2020'>%.0f</font> %s\n",
+ "Info Panel Text - Speed", player.ID,
+ RoundToPowerOfTen(info.Speed, -2),
+ GetTakeoffString(info));
+ }
+ else
+ {
+ FormatEx(speedString, sizeof(speedString),
+ "%T: <font color='#ffffff'>%.0f</font> %s\n",
+ "Info Panel Text - Speed", player.ID,
+ RoundToPowerOfTen(info.Speed, -2),
+ GetTakeoffString(info));
+ }
+ }
+ }
+ return speedString;
+}
+
+static char[] GetTakeoffString(HUDInfo info)
+{
+ char takeoffString[96], duckString[32];
+
+ if (infoPanelShowDuckString[info.ID]
+ || (infoPanelOnGroundLast[info.ID]
+ && !info.HitBhop
+ && info.IsTakeoff
+ && info.Jumped
+ && info.Ducking
+ && (infoPanelDuckPressedLast[info.ID] || GOKZ_GetCoreOption(info.ID, Option_Mode) == Mode_Vanilla)))
+ {
+ duckString = " <font color='#71eeb8'>C</font>";
+ infoPanelShowDuckString[info.ID] = true;
+ }
+ else
+ {
+ duckString = "";
+ infoPanelShowDuckString[info.ID] = false;
+ }
+
+ if (info.HitJB)
+ {
+ FormatEx(takeoffString, sizeof(takeoffString),
+ "(<font color='#ffff20'>%.0f</font>)%s",
+ RoundToPowerOfTen(info.TakeoffSpeed, -2),
+ duckString);
+ }
+ else if (info.HitPerf)
+ {
+ FormatEx(takeoffString, sizeof(takeoffString),
+ "(<font color='#40ff40'>%.0f</font>)%s",
+ RoundToPowerOfTen(info.TakeoffSpeed, -2),
+ duckString);
+ }
+ else
+ {
+ FormatEx(takeoffString, sizeof(takeoffString),
+ "(<font color='#ffffff'>%.0f</font>)%s",
+ RoundToPowerOfTen(info.TakeoffSpeed, -2),
+ duckString);
+ }
+ return takeoffString;
+}
+
+static char[] GetKeysString(KZPlayer player, HUDInfo info)
+{
+ char keysString[64];
+ if (player.ShowKeys == ShowKeys_Disabled)
+ {
+ keysString = "";
+ }
+ else if (player.ShowKeys == ShowKeys_Spectating && player.Alive)
+ {
+ keysString = "";
+ }
+ else
+ {
+ int buttons = info.Buttons;
+ FormatEx(keysString, sizeof(keysString),
+ "%T: <font color='#ffffff'>%c %c %c %c %c %c</font>\n",
+ "Info Panel Text - Keys", player.ID,
+ buttons & IN_MOVELEFT ? 'A' : '_',
+ buttons & IN_FORWARD ? 'W' : '_',
+ buttons & IN_BACK ? 'S' : '_',
+ buttons & IN_MOVERIGHT ? 'D' : '_',
+ buttons & IN_DUCK ? 'C' : '_',
+ buttons & IN_JUMP ? 'J' : '_');
+ }
+ return keysString;
+}
+
+// Credits to Franc1sco (https://github.com/Franc1sco/FixHintColorMessages)
+void PrintCSGOHUDText(int client, const char[] format)
+{
+ char buff[HUD_MAX_HINT_SIZE];
+ Format(buff, sizeof(buff), "</font>%s", format);
+
+ for (int i = strlen(buff); i < sizeof(buff) - 1; i++)
+ {
+ buff[i] = ' ';
+ }
+
+ buff[sizeof(buff) - 1] = '\0';
+
+ Protobuf pb = view_as<Protobuf>(StartMessageOne("TextMsg", client, USERMSG_BLOCKHOOKS));
+ pb.SetInt("msg_dst", 4);
+ pb.AddString("params", "#SFUI_ContractKillStart");
+ pb.AddString("params", buff);
+ pb.AddString("params", NULL_STRING);
+ pb.AddString("params", NULL_STRING);
+ pb.AddString("params", NULL_STRING);
+ pb.AddString("params", NULL_STRING);
+
+ EndMessage();
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/menu.sp b/sourcemod/scripting/gokz-hud/menu.sp
new file mode 100644
index 0000000..4b7259e
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/menu.sp
@@ -0,0 +1,96 @@
+/*
+ Tracks whether a GOKZ HUD menu or panel element is being shown to the client.
+*/
+
+
+
+// Update the TP menu i.e. item text, item disabled/enabled
+void CancelGOKZHUDMenu(int client)
+{
+ // Only cancel the menu if we know it's the TP menu
+ if (gB_MenuShowing[client])
+ {
+ CancelClientMenu(client);
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPlayerSpawn_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnOptionChanged_Menu(int client, HUDOption option)
+{
+ if (option == HUDOption_TPMenu || option == HUDOption_TimerText)
+ {
+ CancelGOKZHUDMenu(client);
+ }
+}
+
+void OnTimerStart_Menu(int client)
+{
+ // Prevent the menu from getting cancelled every tick if player use start timer button zone.
+ if (GOKZ_GetTime(client) > 0.0)
+ {
+ CancelGOKZHUDMenu(client);
+ }
+}
+
+void OnTimerEnd_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnTimerStopped_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnPause_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnResume_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnMakeCheckpoint_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnCountedTeleport_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnJoinTeam_Menu(int client)
+{
+ CancelGOKZHUDMenu(client);
+}
+
+void OnStartPositionSet_Menu(int client)
+{
+ // Prevent the menu from getting cancelled every tick if player use start timer button zone.
+ if (GOKZ_GetTime(client) > 0.0)
+ {
+ CancelGOKZHUDMenu(client);
+ }
+}
+
+void OnPluginEnd_Menu()
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client))
+ {
+ CancelGOKZHUDMenu(client);
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/natives.sp b/sourcemod/scripting/gokz-hud/natives.sp
new file mode 100644
index 0000000..bb877e2
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/natives.sp
@@ -0,0 +1,33 @@
+void CreateNatives()
+{
+ CreateNative("GOKZ_HUD_ForceUpdateTPMenu", Native_ForceUpdateTPMenu);
+ CreateNative("GOKZ_HUD_GetMenuShowing", Native_GetMenuShowing);
+ CreateNative("GOKZ_HUD_SetMenuShowing", Native_SetMenuShowing);
+ CreateNative("GOKZ_HUD_GetMenuSpectatorText", Native_GetSpectatorText);
+}
+
+public int Native_ForceUpdateTPMenu(Handle plugin, int numParams)
+{
+ SetForceUpdateTPMenu(GetNativeCell(1));
+ return 0;
+}
+
+public int Native_GetMenuShowing(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_MenuShowing[GetNativeCell(1)]);
+}
+
+public int Native_SetMenuShowing(Handle plugin, int numParams)
+{
+ gB_MenuShowing[GetNativeCell(1)] = view_as<bool>(GetNativeCell(2));
+ return 0;
+}
+
+public int Native_GetSpectatorText(Handle plugin, int numParams)
+{
+ HUDInfo info;
+ GetNativeArray(2, info, sizeof(HUDInfo));
+ KZPlayer player = KZPlayer(GetNativeCell(1));
+ FormatNativeString(3, 0, 0, GetNativeCell(4), _, "", FormatSpectatorTextForMenu(player, info));
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/options.sp b/sourcemod/scripting/gokz-hud/options.sp
new file mode 100644
index 0000000..84bbf2b
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/options.sp
@@ -0,0 +1,190 @@
+/*
+ Options for controlling appearance and behaviour of HUD and UI.
+*/
+
+
+
+// =====[ EVENTS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void OnOptionChanged_Options(int client, HUDOption option, any newValue)
+{
+ PrintOptionChangeMessage(client, option, newValue);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void RegisterOptions()
+{
+ for (HUDOption option; option < HUDOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_HUDOptionNames[option], gC_HUDOptionDescriptions[option],
+ OptionType_Int, gI_HUDOptionDefaults[option], 0, gI_HUDOptionCounts[option] - 1);
+ }
+}
+
+static void PrintOptionChangeMessage(int client, HUDOption option, any newValue)
+{
+ // NOTE: Not all options have a message for when they are changed.
+ switch (option)
+ {
+ case HUDOption_TPMenu:
+ {
+ switch (newValue)
+ {
+ case TPMenu_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Teleport Menu - Disable");
+ }
+ case TPMenu_Simple:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Teleport Menu - Enable (Simple)");
+ }
+ case TPMenu_Advanced:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Teleport Menu - Enable (Advanced)");
+ }
+ }
+ }
+ case HUDOption_InfoPanel:
+ {
+ switch (newValue)
+ {
+ case InfoPanel_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Info Panel - Disable");
+ }
+ case InfoPanel_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Info Panel - Enable");
+ }
+ }
+ }
+ case HUDOption_TimerStyle:
+ {
+ switch (newValue)
+ {
+ case TimerStyle_Standard:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Timer Style - Standard");
+ }
+ case TimerStyle_Precise:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Timer Style - Precise");
+ }
+ }
+ }
+ case HUDOption_TimerType:
+ {
+ switch (newValue)
+ {
+ case TimerType_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Timer Type - Disabled");
+ }
+ case TimerType_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Timer Type - Enabled");
+ }
+ }
+ }
+ case HUDOption_ShowWeapon:
+ {
+ switch (newValue)
+ {
+ case ShowWeapon_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Weapon - Disable");
+ }
+ case ShowWeapon_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Weapon - Enable");
+ }
+ }
+ }
+ case HUDOption_ShowControls:
+ {
+ switch (newValue)
+ {
+ case ReplayControls_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Controls - Disable");
+ }
+ case ReplayControls_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Controls - Enable");
+ }
+ }
+ }
+ case HUDOption_DeadstrafeColor:
+ {
+ switch (newValue)
+ {
+ case DeadstrafeColor_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Dead Strafe - Disable");
+ }
+ case DeadstrafeColor_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Dead Strafe - Enable");
+ }
+ }
+ }
+ case HUDOption_ShowSpectators:
+ {
+ switch (newValue)
+ {
+ case ShowSpecs_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Spectators - Disable");
+ }
+ case ShowSpecs_Number:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Spectators - Number");
+ }
+ case ShowSpecs_Full:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Spectators - Full");
+ }
+ }
+ }
+ case HUDOption_SpecListPosition:
+ {
+ switch (newValue)
+ {
+ case SpecListPosition_InfoPanel:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Spectator List Position - Info Panel");
+ }
+ case SpecListPosition_TPMenu:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Spectator List Position - TP Menu");
+ }
+ }
+ }
+ case HUDOption_DynamicMenu:
+ {
+ switch (newValue)
+ {
+ case DynamicMenu_Legacy:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Dynamic Menu - Legacy");
+ }
+ case DynamicMenu_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Dynamic Menu - Disable");
+ }
+ case DynamicMenu_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Dynamic Menu - Enable");
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/options_menu.sp b/sourcemod/scripting/gokz-hud/options_menu.sp
new file mode 100644
index 0000000..705df27
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/options_menu.sp
@@ -0,0 +1,181 @@
+static TopMenu optionsTopMenu;
+static TopMenuObject catHUD;
+static TopMenuObject itemsHUD[HUDOPTION_COUNT];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu)
+{
+ if (optionsTopMenu == topMenu && catHUD != INVALID_TOPMENUOBJECT)
+ {
+ return;
+ }
+
+ catHUD = topMenu.AddCategory(HUD_OPTION_CATEGORY, TopMenuHandler_Categories);
+}
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ // Make sure category exists
+ if (catHUD == INVALID_TOPMENUOBJECT)
+ {
+ GOKZ_OnOptionsMenuCreated(topMenu);
+ }
+
+ if (optionsTopMenu == topMenu)
+ {
+ return;
+ }
+
+ optionsTopMenu = topMenu;
+
+ // Add HUD option items
+ for (int option = 0; option < view_as<int>(HUDOPTION_COUNT); option++)
+ {
+ itemsHUD[option] = optionsTopMenu.AddItem(gC_HUDOptionNames[option], TopMenuHandler_HUD, catHUD);
+ }
+}
+
+public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == catHUD)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - HUD", param);
+ }
+ }
+}
+
+public void TopMenuHandler_HUD(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ HUDOption option = HUDOPTION_INVALID;
+ for (int i = 0; i < view_as<int>(HUDOPTION_COUNT); i++)
+ {
+ if (topobj_id == itemsHUD[i])
+ {
+ option = view_as<HUDOption>(i);
+ break;
+ }
+ }
+
+ if (option == HUDOPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case HUDOption_TPMenu:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_TPMenuPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_ShowKeys:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_ShowKeysPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_TimerText:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_TimerTextPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_TimerStyle:
+ {
+ int optionValue = GOKZ_HUD_GetOption(param, option);
+ if (optionValue == TimerStyle_Precise)
+ {
+ FormatEx(buffer, maxlength, "%T - 01:23.45",
+ gC_HUDOptionPhrases[option], param);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - 1:23",
+ gC_HUDOptionPhrases[option], param);
+ }
+ }
+ case HUDOption_TimerType:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_TimerTypePhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_SpeedText:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_SpeedTextPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_ShowControls:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_ShowControlsPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_DeadstrafeColor:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_DeadstrafeColorPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_UpdateRate:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_HUDUpdateRatePhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_ShowSpectators:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_ShowSpecsPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_SpecListPosition:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_SpecListPositionPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ case HUDOption_DynamicMenu:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], param,
+ gC_DynamicMenuPhrases[GOKZ_HUD_GetOption(param, option)], param);
+ }
+ default:FormatToggleableOptionDisplay(param, option, buffer, maxlength);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_HUD_CycleOption(param, option);
+ optionsTopMenu.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void FormatToggleableOptionDisplay(int client, HUDOption option, char[] buffer, int maxlength)
+{
+ if (GOKZ_HUD_GetOption(client, option) == 0)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_HUDOptionPhrases[option], client,
+ "Options Menu - Enabled", client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/racing_text.sp b/sourcemod/scripting/gokz-hud/racing_text.sp
new file mode 100644
index 0000000..ac6ea3d
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/racing_text.sp
@@ -0,0 +1,167 @@
+/*
+ Uses HUD text to show the race countdown and a start message.
+
+ This is manually refreshed when a race starts to show the start message as
+ soon as possible, improving responsiveness.
+*/
+
+
+
+static Handle racingHudSynchronizer;
+static float countdownStartTime[MAXPLAYERS + 1];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_RacingText()
+{
+ racingHudSynchronizer = CreateHudSynchronizer();
+}
+
+void OnPlayerRunCmdPost_RacingText(int client, int cmdnum)
+{
+ int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6;
+ if (gB_GOKZRacing && cmdnum % updateSpeed == 2)
+ {
+ UpdateRacingText(client);
+ }
+}
+
+void OnRaceInfoChanged_RacingText(int raceID, RaceInfo prop, int newValue)
+{
+ if (prop != RaceInfo_Status)
+ {
+ return;
+ }
+
+ if (newValue == RaceStatus_Countdown)
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GOKZ_RC_GetRaceID(client) == raceID)
+ {
+ countdownStartTime[client] = GetGameTime();
+ }
+ }
+ }
+ else if (newValue == RaceStatus_Aborting)
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GOKZ_RC_GetRaceID(client) == raceID)
+ {
+ ClearRacingText(client);
+ }
+ }
+ }
+ else if (newValue == RaceStatus_Started)
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GOKZ_RC_GetRaceID(client) == raceID)
+ {
+ UpdateRacingText(client);
+ }
+ }
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void UpdateRacingText(int client)
+{
+ KZPlayer player = KZPlayer(client);
+
+ if (player.Fake)
+ {
+ return;
+ }
+
+ if (player.Alive)
+ {
+ ShowRacingText(player, player);
+ }
+ else
+ {
+ KZPlayer targetPlayer = KZPlayer(player.ObserverTarget);
+ if (targetPlayer.ID != -1 && !targetPlayer.Fake)
+ {
+ ShowRacingText(player, targetPlayer);
+ }
+ }
+}
+
+static void ClearRacingText(int client)
+{
+ ClearSyncHud(client, racingHudSynchronizer);
+}
+
+static void ShowRacingText(KZPlayer player, KZPlayer targetPlayer)
+{
+ if (GOKZ_RC_GetStatus(targetPlayer.ID) != RacerStatus_Racing)
+ {
+ return;
+ }
+
+ int raceStatus = GOKZ_RC_GetRaceInfo(GOKZ_RC_GetRaceID(targetPlayer.ID), RaceInfo_Status);
+ if (raceStatus == RaceStatus_Countdown)
+ {
+ ShowCountdownText(player, targetPlayer);
+ }
+ else if (raceStatus == RaceStatus_Started)
+ {
+ ShowStartedText(player, targetPlayer);
+ }
+}
+
+static void ShowCountdownText(KZPlayer player, KZPlayer targetPlayer)
+{
+ float timeToStart = (countdownStartTime[targetPlayer.ID] + RC_COUNTDOWN_TIME) - GetGameTime();
+ int colour[4];
+ GetCountdownColour(timeToStart, colour);
+
+ SetHudTextParams(-1.0, 0.3, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0);
+ ShowSyncHudText(player.ID, racingHudSynchronizer, "%t\n\n%d", "Get Ready", IntMax(RoundToCeil(timeToStart), 1));
+}
+
+static void GetCountdownColour(float timeToStart, int buffer[4])
+{
+ // From red to green
+ if (timeToStart >= RC_COUNTDOWN_TIME)
+ {
+ buffer[0] = 255;
+ buffer[1] = 0;
+ }
+ else if (timeToStart > RC_COUNTDOWN_TIME / 2.0)
+ {
+ buffer[0] = 255;
+ buffer[1] = RoundFloat(-510.0 / RC_COUNTDOWN_TIME * timeToStart + 510.0);
+ }
+ else if (timeToStart > 0.0)
+ {
+ buffer[0] = RoundFloat(510.0 / RC_COUNTDOWN_TIME * timeToStart);
+ buffer[1] = 255;
+ }
+ else
+ {
+ buffer[0] = 0;
+ buffer[1] = 255;
+ }
+
+ buffer[2] = 0;
+ buffer[3] = 255;
+}
+
+static void ShowStartedText(KZPlayer player, KZPlayer targetPlayer)
+{
+ if (targetPlayer.TimerRunning)
+ {
+ return;
+ }
+
+ SetHudTextParams(-1.0, 0.3, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), 0, 255, 0, 255, 0, 1.0, 0.0, 0.0);
+ ShowSyncHudText(player.ID, racingHudSynchronizer, "%t", "Go!");
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/spectate_text.sp b/sourcemod/scripting/gokz-hud/spectate_text.sp
new file mode 100644
index 0000000..9700c38
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/spectate_text.sp
@@ -0,0 +1,119 @@
+/*
+ Responsible for spectator list on the HUD.
+*/
+
+#define SPECATATOR_LIST_MAX_COUNT 5
+
+// =====[ PUBLIC ]=====
+
+char[] FormatSpectatorTextForMenu(KZPlayer player, HUDInfo info)
+{
+ int specCount;
+ char spectatorTextString[224];
+ if (player.GetHUDOption(HUDOption_ShowSpectators) >= ShowSpecs_Number)
+ {
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (gI_ObserverTarget[i] == info.ID)
+ {
+ specCount++;
+ if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full)
+ {
+ char buffer[64];
+ if (specCount < SPECATATOR_LIST_MAX_COUNT)
+ {
+ GetClientName(i, buffer, sizeof(buffer));
+ Format(spectatorTextString, sizeof(spectatorTextString), "%s\n%s", spectatorTextString, buffer);
+ }
+ else if (specCount == SPECATATOR_LIST_MAX_COUNT)
+ {
+ StrCat(spectatorTextString, sizeof(spectatorTextString), "\n...");
+ }
+ }
+ }
+ }
+ if (specCount > 0)
+ {
+ if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full)
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), "%t\n ", "Spectator List - Menu (Full)", specCount, spectatorTextString);
+ }
+ else
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), "%t\n ", "Spectator List - Menu (Number)", specCount);
+ }
+ }
+ else
+ {
+ FormatEx(spectatorTextString, sizeof(spectatorTextString), "");
+ }
+ }
+ return spectatorTextString;
+}
+
+char[] FormatSpectatorTextForInfoPanel(KZPlayer player, KZPlayer targetPlayer)
+{
+ int specCount;
+ char spectatorTextString[160];
+ if (player.GetHUDOption(HUDOption_ShowSpectators) >= ShowSpecs_Number)
+ {
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (gI_ObserverTarget[i] == targetPlayer.ID)
+ {
+ specCount++;
+ if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full)
+ {
+ char buffer[64];
+ if (specCount < SPECATATOR_LIST_MAX_COUNT)
+ {
+ GetClientName(i, buffer, sizeof(buffer));
+ if (specCount == 1)
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), "%s", buffer);
+ }
+ else
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), "%s, %s", spectatorTextString, buffer);
+ }
+ }
+ else if (specCount == SPECATATOR_LIST_MAX_COUNT)
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), " ...");
+ }
+ }
+ }
+ }
+ if (specCount > 0)
+ {
+ if (player.GetHUDOption(HUDOption_ShowSpectators) == ShowSpecs_Full)
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), "%t\n", "Spectator List - Info Panel (Full)", specCount, spectatorTextString);
+ }
+ else
+ {
+ Format(spectatorTextString, sizeof(spectatorTextString), "%t\n", "Spectator List - Info Panel (Number)", specCount);
+ }
+ }
+ else
+ {
+ FormatEx(spectatorTextString, sizeof(spectatorTextString), "");
+ }
+ }
+ return spectatorTextString;
+}
+
+void UpdateSpecList()
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ gI_ObserverTarget[client] = GetObserverTarget(client);
+ }
+ else
+ {
+ gI_ObserverTarget[client] = -1;
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/speed_text.sp b/sourcemod/scripting/gokz-hud/speed_text.sp
new file mode 100644
index 0000000..3f83949
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/speed_text.sp
@@ -0,0 +1,141 @@
+/*
+ Uses HUD text to show current speed somewhere on the screen.
+
+ This is manually refreshed whenever player has taken off so that they see
+ their pre-speed as soon as possible, improving responsiveness.
+*/
+
+
+
+static Handle speedHudSynchronizer;
+
+static bool speedTextDuckPressedLast[MAXPLAYERS + 1];
+static bool speedTextOnGroundLast[MAXPLAYERS + 1];
+static bool speedTextShowDuckString[MAXPLAYERS + 1];
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_SpeedText()
+{
+ speedHudSynchronizer = CreateHudSynchronizer();
+}
+
+void OnPlayerRunCmdPost_SpeedText(int client, int cmdnum, HUDInfo info)
+{
+ int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6;
+ if (cmdnum % updateSpeed == 0 || info.IsTakeoff)
+ {
+ UpdateSpeedText(client, info);
+ }
+ speedTextOnGroundLast[info.ID] = info.OnGround;
+ speedTextDuckPressedLast[info.ID] = info.Ducking;
+}
+
+void OnOptionChanged_SpeedText(int client, HUDOption option)
+{
+ if (option == HUDOption_SpeedText)
+ {
+ ClearSpeedText(client);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void UpdateSpeedText(int client, HUDInfo info)
+{
+ KZPlayer player = KZPlayer(client);
+
+ if (player.Fake
+ || player.SpeedText != SpeedText_Bottom)
+ {
+ return;
+ }
+
+ ShowSpeedText(player, info);
+}
+
+static void ClearSpeedText(int client)
+{
+ ClearSyncHud(client, speedHudSynchronizer);
+}
+
+static void ShowSpeedText(KZPlayer player, HUDInfo info)
+{
+ if (info.Paused)
+ {
+ return;
+ }
+
+ int colour[4] = { 255, 255, 255, 0 }; // RGBA
+ float velZ = Movement_GetVerticalVelocity(info.ID);
+ if (!info.OnGround && !info.OnLadder && !info.Noclipping)
+ {
+ if (GOKZ_HUD_GetOption(player.ID, HUDOption_DeadstrafeColor) == DeadstrafeColor_Enabled && velZ > 0.0 && velZ < 140.0)
+ {
+ colour = { 255, 32, 32, 0 };
+ }
+ else if (info.HitPerf)
+ {
+ if (info.HitJB)
+ {
+ colour = { 255, 255, 32, 0 };
+ }
+ else
+ {
+ colour = { 64, 255, 64, 0 };
+ }
+ }
+ }
+
+ switch (player.SpeedText)
+ {
+ case SpeedText_Bottom:
+ {
+ // Set params based on the available screen space at max scaling HUD
+ if (!IsDrawingInfoPanel(player.ID))
+ {
+ SetHudTextParams(-1.0, 0.75, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0);
+ }
+ else
+ {
+ SetHudTextParams(-1.0, 0.65, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0);
+ }
+ }
+ }
+
+ if (info.OnGround || info.OnLadder || info.Noclipping)
+ {
+ ShowSyncHudText(player.ID, speedHudSynchronizer,
+ "%.0f",
+ RoundFloat(info.Speed * 10) / 10.0);
+ speedTextShowDuckString[info.ID] = false;
+ }
+ else
+ {
+ if (speedTextShowDuckString[info.ID]
+ || (speedTextOnGroundLast[info.ID]
+ && !info.HitBhop
+ && info.IsTakeoff
+ && info.Jumped
+ && info.Ducking
+ && (speedTextDuckPressedLast[info.ID] || GOKZ_GetCoreOption(info.ID, Option_Mode) == Mode_Vanilla)))
+ {
+ ShowSyncHudText(player.ID, speedHudSynchronizer,
+ "%.0f\n (%.0f)C",
+ RoundToPowerOfTen(info.Speed, -2),
+ RoundToPowerOfTen(info.TakeoffSpeed, -2));
+ speedTextShowDuckString[info.ID] = true;
+ }
+ else {
+ ShowSyncHudText(player.ID, speedHudSynchronizer,
+ "%.0f\n(%.0f)",
+ RoundToPowerOfTen(info.Speed, -2),
+ RoundToPowerOfTen(info.TakeoffSpeed, -2));
+ speedTextShowDuckString[info.ID] = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/timer_text.sp b/sourcemod/scripting/gokz-hud/timer_text.sp
new file mode 100644
index 0000000..a5fd17b
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/timer_text.sp
@@ -0,0 +1,135 @@
+/*
+ Uses HUD text to show current run time somewhere on the screen.
+
+ This is manually refreshed whenever the players' timer is started, ended or
+ stopped to improve responsiveness.
+*/
+
+
+
+static Handle timerHudSynchronizer;
+
+
+
+// =====[ PUBLIC ]=====
+
+char[] FormatTimerTextForMenu(KZPlayer player, HUDInfo info)
+{
+ char timerTextString[32];
+ if (info.TimerRunning)
+ {
+ if (player.GetHUDOption(HUDOption_TimerType) == TimerType_Enabled)
+ {
+ FormatEx(timerTextString, sizeof(timerTextString),
+ "%s %s",
+ gC_TimeTypeNames[info.TimeType],
+ GOKZ_HUD_FormatTime(player.ID, info.Time));
+ }
+ else
+ {
+ FormatEx(timerTextString, sizeof(timerTextString),
+ "%s",
+ GOKZ_HUD_FormatTime(player.ID, info.Time));
+ }
+ if (info.Paused)
+ {
+ Format(timerTextString, sizeof(timerTextString), "%s (%T)", timerTextString, "Info Panel Text - PAUSED", player.ID);
+ }
+ }
+ return timerTextString;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_TimerText()
+{
+ timerHudSynchronizer = CreateHudSynchronizer();
+}
+
+void OnPlayerRunCmdPost_TimerText(int client, int cmdnum, HUDInfo info)
+{
+ int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6;
+ if (cmdnum % updateSpeed == 1)
+ {
+ UpdateTimerText(client, info);
+ }
+}
+
+void OnOptionChanged_TimerText(int client, HUDOption option)
+{
+ if (option == HUDOption_TimerText)
+ {
+ ClearTimerText(client);
+ }
+}
+
+void OnTimerEnd_TimerText(int client)
+{
+ ClearTimerText(client);
+}
+
+void OnTimerStopped_TimerText(int client)
+{
+ ClearTimerText(client);
+}
+
+
+// =====[ PRIVATE ]=====
+
+static void UpdateTimerText(int client, HUDInfo info)
+{
+ KZPlayer player = KZPlayer(client);
+
+ if (player.Fake)
+ {
+ return;
+ }
+
+ ShowTimerText(player, info);
+}
+
+static void ClearTimerText(int client)
+{
+ ClearSyncHud(client, timerHudSynchronizer);
+}
+
+static void ShowTimerText(KZPlayer player, HUDInfo info)
+{
+ if (!info.TimerRunning)
+ {
+ if (player.ID != info.ID)
+ {
+ CancelGOKZHUDMenu(player.ID);
+ }
+ return;
+ }
+ if (player.TimerText == TimerText_Top || player.TimerText == TimerText_Bottom)
+ {
+ int colour[4]; // RGBA
+ if (player.GetHUDOption(HUDOption_TimerType) == TimerType_Enabled)
+ {
+ switch (info.TimeType)
+ {
+ case TimeType_Nub:colour = { 234, 209, 138, 0 };
+ case TimeType_Pro:colour = { 181, 212, 238, 0 };
+ }
+ }
+ else colour = { 255, 255, 255, 0};
+
+ switch (player.TimerText)
+ {
+ case TimerText_Top:
+ {
+ SetHudTextParams(-1.0, 0.07, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0);
+ }
+ case TimerText_Bottom:
+ {
+ SetHudTextParams(-1.0, 0.9, GetTextHoldTime(gB_FastUpdateRate[player.ID] ? 3 : 6), colour[0], colour[1], colour[2], colour[3], 0, 1.0, 0.0, 0.0);
+ }
+ }
+
+ ShowSyncHudText(player.ID, timerHudSynchronizer, GOKZ_HUD_FormatTime(player.ID, info.Time));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-hud/tp_menu.sp b/sourcemod/scripting/gokz-hud/tp_menu.sp
new file mode 100644
index 0000000..bb78f1e
--- /dev/null
+++ b/sourcemod/scripting/gokz-hud/tp_menu.sp
@@ -0,0 +1,415 @@
+/*
+ Lets players easily use teleport functionality.
+
+ This menu is displayed whenever the player is alive and there is
+ currently no other menu displaying.
+*/
+
+
+
+#define ITEM_INFO_CHECKPOINT "cp"
+#define ITEM_INFO_TELEPORT "tp"
+#define ITEM_INFO_PREV "prev"
+#define ITEM_INFO_NEXT "next"
+#define ITEM_INFO_UNDO "undo"
+#define ITEM_INFO_PAUSE "pause"
+#define ITEM_INFO_START "start"
+
+static bool oldCanMakeCP[MAXPLAYERS + 1];
+static bool oldCanTP[MAXPLAYERS + 1];
+static bool oldCanPrevCP[MAXPLAYERS + 1];
+static bool oldCanNextCP[MAXPLAYERS + 1];
+static bool oldCanUndoTP[MAXPLAYERS + 1];
+static bool oldCanPause[MAXPLAYERS + 1];
+static bool oldCanResume[MAXPLAYERS + 1];
+static bool forceRefresh[MAXPLAYERS + 1];
+
+// =====[ EVENTS ]=====
+
+void OnPlayerRunCmdPost_TPMenu(int client, int cmdnum, HUDInfo info)
+{
+ int updateSpeed = gB_FastUpdateRate[client] ? 3 : 6;
+ if (cmdnum % updateSpeed == 2)
+ {
+ UpdateTPMenu(client, info);
+ }
+}
+
+public int PanelHandler_Menu(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Cancel)
+ {
+ gB_MenuShowing[param1] = false;
+ }
+ return 0;
+}
+
+public int MenuHandler_TPMenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_CHECKPOINT, false))
+ {
+ GOKZ_MakeCheckpoint(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_TELEPORT, false))
+ {
+ GOKZ_TeleportToCheckpoint(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_PREV, false))
+ {
+ GOKZ_PrevCheckpoint(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_NEXT, false))
+ {
+ GOKZ_NextCheckpoint(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_UNDO, false))
+ {
+ GOKZ_UndoTeleport(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_PAUSE, false))
+ {
+ GOKZ_TogglePause(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_START, false))
+ {
+ GOKZ_TeleportToStart(param1);
+ }
+
+ // Menu closes when player selects something, so...
+ gB_MenuShowing[param1] = false;
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ gB_MenuShowing[param1] = false;
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+// =====[ PUBLIC ]=====
+void SetForceUpdateTPMenu(int client)
+{
+ forceRefresh[client] = true;
+}
+
+// =====[ PRIVATE ]=====
+
+static void UpdateTPMenu(int client, HUDInfo info)
+{
+ KZPlayer player = KZPlayer(client);
+
+ if (player.Fake)
+ {
+ return;
+ }
+
+ bool force = forceRefresh[client]
+ || player.CanMakeCheckpoint != oldCanMakeCP[client]
+ || player.CanTeleportToCheckpoint != oldCanTP[client]
+ || player.CanPrevCheckpoint != oldCanPrevCP[client]
+ || player.CanNextCheckpoint != oldCanNextCP[client]
+ || player.CanUndoTeleport != oldCanUndoTP[client]
+ || player.CanPause != oldCanPause[client]
+ || player.CanResume != oldCanResume[client];
+
+
+ if (player.Alive)
+ {
+ if (player.TPMenu != TPMenu_Disabled)
+ {
+ if (GetClientMenu(client) == MenuSource_None
+ || gB_MenuShowing[player.ID] && GetClientAvgLoss(player.ID, NetFlow_Both) > EPSILON
+ || gB_MenuShowing[player.ID] && player.TimerRunning && !player.Paused && player.TimerText == TimerText_TPMenu
+ || gB_MenuShowing[player.ID] && force)
+ {
+ ShowTPMenu(player, info);
+ }
+ }
+ else
+ {
+ // There is no need to update this very often as there's no menu selection to be done here.
+ if (GetClientMenu(client) == MenuSource_None
+ || gB_MenuShowing[player.ID] && player.TimerRunning && !player.Paused && player.TimerText == TimerText_TPMenu)
+ {
+ ShowPanel(player, info);
+ }
+ }
+ }
+ else if (player.ObserverTarget != -1) // If the player is spectating someone else
+ {
+ // Check if the replay plugin wants to display the replay control menu.
+ if (!(IsFakeClient(player.ObserverTarget) && gB_GOKZReplays && GOKZ_RP_UpdateReplayControlMenu(client)))
+ {
+ ShowPanel(player, info);
+ }
+ }
+
+ oldCanMakeCP[client] = player.CanMakeCheckpoint;
+ oldCanTP[client] = player.CanTeleportToCheckpoint;
+ oldCanPrevCP[client] = player.CanPrevCheckpoint;
+ oldCanNextCP[client] = player.CanNextCheckpoint;
+ oldCanUndoTP[client] = player.CanUndoTeleport;
+ oldCanPause[client] = player.CanPause;
+ oldCanResume[client] = player.CanResume;
+ forceRefresh[client] = false;
+}
+
+static void ShowPanel(KZPlayer player, HUDInfo info)
+{
+ char panelTitle[256];
+ // Spectator List
+ if (player.ShowSpectators >= ShowSpecs_Number && player.SpecListPosition == SpecListPosition_TPMenu)
+ {
+ Format(panelTitle, sizeof(panelTitle), "%s", FormatSpectatorTextForMenu(player, info));
+ }
+ // Timer panel
+ if (player.TimerText == TimerText_TPMenu && info.TimerRunning)
+ {
+ if (panelTitle[0] != '\0')
+ {
+ Format(panelTitle, sizeof(panelTitle), "%s \n%s", panelTitle, FormatTimerTextForMenu(player, info));
+ }
+ else
+ {
+ Format(panelTitle, sizeof(panelTitle), "%s", FormatTimerTextForMenu(player, info));
+ }
+ if (info.TimeType == TimeType_Nub && info.CurrentTeleport != 0)
+ {
+ Format(panelTitle, sizeof(panelTitle), "%s\n%t", panelTitle, "TP Menu - Spectator Teleports", info.CurrentTeleport);
+ }
+ }
+
+ if (panelTitle[0] != '\0' && GetClientMenu(player.ID) == MenuSource_None || gB_MenuShowing[player.ID])
+ {
+ Panel panel = new Panel(null);
+ panel.SetTitle(panelTitle);
+ panel.Send(player.ID, PanelHandler_Menu, MENU_TIME_FOREVER);
+
+ delete panel;
+ gB_MenuShowing[player.ID] = true;
+ }
+}
+
+static void ShowTPMenu(KZPlayer player, HUDInfo info)
+{
+ Menu menu = new Menu(MenuHandler_TPMenu);
+ menu.OptionFlags = MENUFLAG_NO_SOUND;
+ menu.ExitButton = false;
+ menu.Pagination = MENU_NO_PAGINATION;
+ TPMenuSetTitle(player, menu, info);
+ TPMenuAddItems(player, menu);
+ menu.Display(player.ID, MENU_TIME_FOREVER);
+ gB_MenuShowing[player.ID] = true;
+}
+
+static void TPMenuSetTitle(KZPlayer player, Menu menu, HUDInfo info)
+{
+ char title[256];
+ if (player.ShowSpectators >= ShowSpecs_Number && player.SpecListPosition == SpecListPosition_TPMenu)
+ {
+ Format(title, sizeof(title), "%s", FormatSpectatorTextForMenu(player, info));
+ }
+ if (player.TimerRunning && player.TimerText == TimerText_TPMenu)
+ {
+ if (title[0] != '\0')
+ {
+ Format(title, sizeof(title), "%s \n%s", title, FormatTimerTextForMenu(player, info));
+ }
+ else
+ {
+ Format(title, sizeof(title), "%s", FormatTimerTextForMenu(player, info));
+ }
+ }
+ if (title[0] != '\0')
+ {
+ menu.SetTitle(title);
+ }
+}
+
+static void TPMenuAddItems(KZPlayer player, Menu menu)
+{
+ switch (player.TPMenu)
+ {
+ case TPMenu_Simple:
+ {
+ TPMenuAddItemCheckpoint(player, menu);
+ TPMenuAddItemTeleport(player, menu);
+ TPMenuAddItemPause(player, menu);
+ TPMenuAddItemStart(player, menu);
+ }
+ case TPMenu_Advanced:
+ {
+ TPMenuAddItemCheckpoint(player, menu);
+ TPMenuAddItemTeleport(player, menu);
+ TPMenuAddItemPrevCheckpoint(player, menu);
+ TPMenuAddItemNextCheckpoint(player, menu);
+ TPMenuAddItemUndo(player, menu);
+ TPMenuAddItemPause(player, menu);
+ TPMenuAddItemStart(player, menu);
+ }
+ }
+}
+
+static void TPMenuAddItemCheckpoint(KZPlayer player, Menu menu)
+{
+ char display[24];
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Checkpoint", player.ID);
+ if (player.TimerRunning)
+ {
+ Format(display, sizeof(display), "%s #%d", display, player.CheckpointCount);
+ }
+
+ // Legacy behavior: Always able to make checkpoint attempts.
+ if (gI_DynamicMenu[player.ID] == DynamicMenu_Enabled && !player.CanMakeCheckpoint)
+ {
+ menu.AddItem(ITEM_INFO_CHECKPOINT, display, ITEMDRAW_DISABLED);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_CHECKPOINT, display, ITEMDRAW_DEFAULT);
+ }
+
+}
+
+static void TPMenuAddItemTeleport(KZPlayer player, Menu menu)
+{
+ char display[24];
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Teleport", player.ID);
+ if (player.TimerRunning)
+ {
+ Format(display, sizeof(display), "%s #%d", display, player.TeleportCount);
+ }
+
+ // Legacy behavior: Only able to make TP attempts when there is a checkpoint.
+ if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanTeleportToCheckpoint)
+ {
+ menu.AddItem(ITEM_INFO_TELEPORT, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_TELEPORT, display, ITEMDRAW_DISABLED);
+ }
+}
+
+static void TPMenuAddItemPrevCheckpoint(KZPlayer player, Menu menu)
+{
+ char display[24];
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Prev CP", player.ID);
+
+ // Legacy behavior: Only able to do prev CP when there is a previous checkpoint to go back to.
+ if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanPrevCheckpoint)
+ {
+ menu.AddItem(ITEM_INFO_PREV, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_PREV, display, ITEMDRAW_DISABLED);
+ }
+}
+
+static void TPMenuAddItemNextCheckpoint(KZPlayer player, Menu menu)
+{
+ char display[24];
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Next CP", player.ID);
+
+ // Legacy behavior: Only able to do prev CP when there is a next checkpoint to go forward to.
+ if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanNextCheckpoint)
+ {
+ menu.AddItem(ITEM_INFO_NEXT, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_NEXT, display, ITEMDRAW_DISABLED);
+ }
+}
+
+static void TPMenuAddItemUndo(KZPlayer player, Menu menu)
+{
+ char display[24];
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Undo TP", player.ID);
+
+ // Legacy behavior: Only able to attempt to undo TP when it is allowed.
+ if (gI_DynamicMenu[player.ID] == DynamicMenu_Disabled || player.CanUndoTeleport)
+ {
+ menu.AddItem(ITEM_INFO_UNDO, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_UNDO, display, ITEMDRAW_DISABLED);
+ }
+
+}
+
+static void TPMenuAddItemPause(KZPlayer player, Menu menu)
+{
+ char display[24];
+
+ // Legacy behavior: Always able to attempt to pause.
+ if (gI_DynamicMenu[player.ID] == DynamicMenu_Enabled)
+ {
+ if (player.Paused)
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Resume", player.ID);
+ if (player.CanResume)
+ {
+ menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DISABLED);
+ }
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Pause", player.ID);
+ if (player.CanPause)
+ {
+ menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DISABLED);
+ }
+ }
+ }
+ else
+ {
+ if (player.Paused)
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Resume", player.ID);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Pause", player.ID);
+ }
+ menu.AddItem(ITEM_INFO_PAUSE, display, ITEMDRAW_DEFAULT);
+ }
+}
+
+static void TPMenuAddItemStart(KZPlayer player, Menu menu)
+{
+ char display[24];
+ if (player.StartPositionType == StartPositionType_Spawn)
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Respawn", player.ID);
+ menu.AddItem(ITEM_INFO_START, display, ITEMDRAW_DEFAULT);
+ }
+ else if (player.TimerRunning)
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Restart", player.ID);
+ menu.AddItem(ITEM_INFO_START, display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "TP Menu - Start", player.ID);
+ menu.AddItem(ITEM_INFO_START, display, ITEMDRAW_DEFAULT);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-jumpbeam.sp b/sourcemod/scripting/gokz-jumpbeam.sp
new file mode 100644
index 0000000..9f5b48d
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpbeam.sp
@@ -0,0 +1,325 @@
+#include <sourcemod>
+
+#include <sdktools>
+
+#include <gokz/core>
+#include <gokz/jumpbeam>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Jump Beam",
+ author = "DanZay",
+ description = "Provides option to leave behind a trail when in midair",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-jumpbeam.txt"
+
+float gF_OldOrigin[MAXPLAYERS + 1][3];
+bool gB_OldDucking[MAXPLAYERS + 1];
+int gI_BeamModel;
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatGeneral;
+TopMenuObject gTMO_ItemsJB[JBOPTION_COUNT];
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-jumpbeam");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-jumpbeam.phrases");
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+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_JumpBeam(client);
+ UpdateOldVariables(client);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ gI_BeamModel = PrecacheModel("materials/sprites/laserbeam.vmt", true);
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+
+
+// =====[ GENERAL ]=====
+
+void UpdateOldVariables(int client)
+{
+ if (IsPlayerAlive(client))
+ {
+ Movement_GetOrigin(client, gF_OldOrigin[client]);
+ gB_OldDucking[client] = Movement_GetDucking(client);
+ }
+}
+
+
+
+// =====[ JUMP BEAM ]=====
+
+void OnPlayerRunCmdPost_JumpBeam(int targetClient)
+{
+ // In this case, spectators are handled from the target
+ // client's OnPlayerRunCmd call, otherwise the jump
+ // beam will be all broken up.
+
+ KZPlayer targetPlayer = KZPlayer(targetClient);
+
+ if (targetPlayer.Fake || !targetPlayer.Alive || targetPlayer.OnGround || !targetPlayer.ValidJump)
+ {
+ return;
+ }
+
+ // Send to self
+ SendJumpBeam(targetPlayer, targetPlayer);
+
+ // Send to spectators
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ KZPlayer player = KZPlayer(client);
+ if (player.InGame && !player.Alive && player.ObserverTarget == targetClient)
+ {
+ SendJumpBeam(player, targetPlayer);
+ }
+ }
+}
+
+void SendJumpBeam(KZPlayer player, KZPlayer targetPlayer)
+{
+ if (player.JBType == JBType_Disabled)
+ {
+ return;
+ }
+
+ switch (player.JBType)
+ {
+ case JBType_Feet:SendFeetJumpBeam(player, targetPlayer);
+ case JBType_Head:SendHeadJumpBeam(player, targetPlayer);
+ case JBType_FeetAndHead:
+ {
+ SendFeetJumpBeam(player, targetPlayer);
+ SendHeadJumpBeam(player, targetPlayer);
+ }
+ case JBType_Ground:SendGroundJumpBeam(player, targetPlayer);
+ }
+}
+
+void SendFeetJumpBeam(KZPlayer player, KZPlayer targetPlayer)
+{
+ float origin[3], beamStart[3], beamEnd[3];
+ int beamColour[4];
+ targetPlayer.GetOrigin(origin);
+
+ beamStart = gF_OldOrigin[targetPlayer.ID];
+ beamEnd = origin;
+ GetJumpBeamColour(targetPlayer, beamColour);
+
+ TE_SetupBeamPoints(beamStart, beamEnd, gI_BeamModel, 0, 0, 0, JB_BEAM_LIFETIME, 0.25, 0.25, 10, 0.0, beamColour, 0);
+ TE_SendToClient(player.ID);
+}
+
+void SendHeadJumpBeam(KZPlayer player, KZPlayer targetPlayer)
+{
+ float origin[3], beamStart[3], beamEnd[3];
+ int beamColour[4];
+ targetPlayer.GetOrigin(origin);
+
+ beamStart = gF_OldOrigin[targetPlayer.ID];
+ beamEnd = origin;
+ if (gB_OldDucking[targetPlayer.ID])
+ {
+ beamStart[2] = beamStart[2] + 54.0;
+ }
+ else
+ {
+ beamStart[2] = beamStart[2] + 72.0;
+ }
+ if (targetPlayer.Ducking)
+ {
+ beamEnd[2] = beamEnd[2] + 54.0;
+ }
+ else
+ {
+ beamEnd[2] = beamEnd[2] + 72.0;
+ }
+ GetJumpBeamColour(targetPlayer, beamColour);
+
+ TE_SetupBeamPoints(beamStart, beamEnd, gI_BeamModel, 0, 0, 0, JB_BEAM_LIFETIME, 0.25, 0.25, 10, 0.0, beamColour, 0);
+ TE_SendToClient(player.ID);
+}
+
+void SendGroundJumpBeam(KZPlayer player, KZPlayer targetPlayer)
+{
+ float origin[3], takeoffOrigin[3], beamStart[3], beamEnd[3];
+ int beamColour[4];
+ targetPlayer.GetOrigin(origin);
+ targetPlayer.GetTakeoffOrigin(takeoffOrigin);
+
+ beamStart = gF_OldOrigin[targetPlayer.ID];
+ beamEnd = origin;
+ beamStart[2] = takeoffOrigin[2] + 0.1;
+ beamEnd[2] = takeoffOrigin[2] + 0.1;
+ GetJumpBeamColour(targetPlayer, beamColour);
+
+ TE_SetupBeamPoints(beamStart, beamEnd, gI_BeamModel, 0, 0, 0, JB_BEAM_LIFETIME, 0.25, 0.25, 10, 0.0, beamColour, 0);
+ TE_SendToClient(player.ID);
+}
+
+void GetJumpBeamColour(KZPlayer targetPlayer, int colour[4])
+{
+ float velocity[3];
+ targetPlayer.GetVelocity(velocity);
+ if (targetPlayer.Ducking)
+ {
+ colour = { 255, 0, 0, 110 }; // Red
+ }
+ else if( velocity[2] < 140 && velocity[2] > 0 ) {
+ colour = { 255, 220, 0, 110 };
+ }
+ else
+ {
+ colour = { 0, 255, 0, 110 }; // Green
+ }
+}
+
+
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void RegisterOptions()
+{
+ for (JBOption option; option < JBOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_JBOptionNames[option], gC_JBOptionDescriptions[option],
+ OptionType_Int, gI_JBOptionDefaultValues[option], 0, gI_JBOptionCounts[option] - 1);
+ }
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+ gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY);
+
+ for (int option = 0; option < view_as<int>(JBOPTION_COUNT); option++)
+ {
+ gTMO_ItemsJB[option] = gTM_Options.AddItem(gC_JBOptionNames[option], TopMenuHandler_General, gTMO_CatGeneral);
+ }
+}
+
+public void TopMenuHandler_General(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ JBOption option = JBOPTION_INVALID;
+ for (int i = 0; i < view_as<int>(JBOPTION_COUNT); i++)
+ {
+ if (topobj_id == gTMO_ItemsJB[i])
+ {
+ option = view_as<JBOption>(i);
+ break;
+ }
+ }
+
+ if (option == JBOPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case JBOption_Type:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_JBOptionPhrases[option], param,
+ gC_JBTypePhrases[GOKZ_JB_GetOption(param, option)], param);
+ }
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ switch (option)
+ {
+ default:
+ {
+ GOKZ_JB_CycleOption(param, option);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-jumpstats.sp b/sourcemod/scripting/gokz-jumpstats.sp
new file mode 100644
index 0000000..74d9220
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats.sp
@@ -0,0 +1,216 @@
+#include <sourcemod>
+
+#include <dhooks>
+#include <sdkhooks>
+#include <sdktools>
+
+#include <gokz/core>
+#include <gokz/jumpstats>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/localdb>
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Jumpstats",
+ author = "DanZay",
+ description = "Tracks and outputs movement statistics",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-jumpstats.txt"
+
+// This must be global because it's both used by jump tracking and validating.
+bool gB_SpeedJustModifiedExternally[MAXPLAYERS + 1];
+
+#include "gokz-jumpstats/api.sp"
+#include "gokz-jumpstats/commands.sp"
+#include "gokz-jumpstats/distance_tiers.sp"
+#include "gokz-jumpstats/jump_reporting.sp"
+#include "gokz-jumpstats/jump_tracking.sp"
+#include "gokz-jumpstats/jump_validating.sp"
+#include "gokz-jumpstats/options.sp"
+#include "gokz-jumpstats/options_menu.sp"
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-jumpstats");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-jumpstats.phrases");
+
+ LoadBroadcastTiers();
+ CreateGlobalForwards();
+ RegisterCommands();
+
+ OnPluginStart_JumpTracking();
+ OnPluginStart_JumpValidating();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+
+ 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);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ HookClientEvents(client);
+ OnClientPutInServer_Options(client);
+ OnClientPutInServer_JumpTracking(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])
+{
+ OnPlayerRunCmd_JumpTracking(client, buttons, tickcount);
+ 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])
+{
+ OnPlayerRunCmdPost_JumpTracking(client);
+}
+
+public void Movement_OnStartTouchGround(int client)
+{
+ OnStartTouchGround_JumpTracking(client);
+}
+
+public void GOKZ_OnJumpInvalidated(int client)
+{
+ OnJumpInvalidated_JumpTracking(client);
+}
+
+public void GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ OnJumpValidated_JumpTracking(client, jumped, ladderJump, jumpbug);
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ OnOptionChanged_JumpTracking(client, option);
+ OnOptionChanged_Options(client, option, newValue);
+}
+
+public void GOKZ_JS_OnLanding(Jump jump)
+{
+ OnLanding_JumpReporting(jump);
+}
+
+public void GOKZ_JS_OnFailstat(Jump jump)
+{
+ OnFailstat_FailstatReporting(jump);
+}
+
+public void GOKZ_JS_OnJumpstatAlways(Jump jump)
+{
+ OnJumpstatAlways_JumpstatAlwaysReporting(jump);
+}
+
+public void GOKZ_JS_OnFailstatAlways(Jump jump)
+{
+ OnFailstatAlways_FailstatAlwaysReporting(jump);
+}
+
+public void SDKHook_StartTouch_Callback(int client, int touched) // SDKHook_StartTouchPost
+{
+ OnStartTouch_JumpTracking(client, touched);
+}
+
+public void SDKHook_Touch_CallBack(int client, int touched)
+{
+ OnTouch_JumpTracking(client);
+}
+
+public void SDKHook_EndTouch_Callback(int client, int touched) // SDKHook_EndTouchPost
+{
+ OnEndTouch_JumpTracking(client, touched);
+}
+
+public void GOKZ_OnTeleport(int client)
+{
+ OnTeleport_FailstatAlways(client);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ OnMapStart_JumpReporting();
+ OnMapStart_DistanceTiers();
+}
+
+public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ OnOptionsMenuCreated_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnSlap(int client)
+{
+ InvalidateJumpstat(client);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void HookClientEvents(int client)
+{
+ SDKHook(client, SDKHook_StartTouchPost, SDKHook_StartTouch_Callback);
+ SDKHook(client, SDKHook_TouchPost, SDKHook_Touch_CallBack);
+ SDKHook(client, SDKHook_EndTouchPost, SDKHook_EndTouch_Callback);
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/api.sp b/sourcemod/scripting/gokz-jumpstats/api.sp
new file mode 100644
index 0000000..2625fda
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/api.sp
@@ -0,0 +1,78 @@
+static GlobalForward H_OnTakeoff;
+static GlobalForward H_OnLanding;
+static GlobalForward H_OnFailstat;
+static GlobalForward H_OnJumpstatAlways;
+static GlobalForward H_OnFailstatAlways;
+static GlobalForward H_OnJumpInvalidated;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnTakeoff = new GlobalForward("GOKZ_JS_OnTakeoff", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnLanding = new GlobalForward("GOKZ_JS_OnLanding", ET_Ignore, Param_Array);
+ H_OnFailstat = new GlobalForward("GOKZ_JS_OnFailstat", ET_Ignore, Param_Array);
+ H_OnJumpstatAlways = new GlobalForward("GOKZ_JS_OnJumpstatAlways", ET_Ignore, Param_Array);
+ H_OnFailstatAlways = new GlobalForward("GOKZ_JS_OnFailstatAlways", ET_Ignore, Param_Array);
+ H_OnJumpInvalidated = new GlobalForward("GOKZ_JS_OnJumpInvalidated", ET_Ignore, Param_Cell);
+}
+
+void Call_OnTakeoff(int client, int jumpType)
+{
+ Call_StartForward(H_OnTakeoff);
+ Call_PushCell(client);
+ Call_PushCell(jumpType);
+ Call_Finish();
+}
+
+void Call_OnLanding(Jump jump)
+{
+ Call_StartForward(H_OnLanding);
+ Call_PushArray(jump, sizeof(jump));
+ Call_Finish();
+}
+
+void Call_OnJumpInvalidated(int client)
+{
+ Call_StartForward(H_OnJumpInvalidated);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_OnFailstat(Jump jump)
+{
+ Call_StartForward(H_OnFailstat);
+ Call_PushArray(jump, sizeof(jump));
+ Call_Finish();
+}
+
+void Call_OnJumpstatAlways(Jump jump)
+{
+ Call_StartForward(H_OnJumpstatAlways);
+ Call_PushArray(jump, sizeof(jump));
+ Call_Finish();
+}
+
+void Call_OnFailstatAlways(Jump jump)
+{
+ Call_StartForward(H_OnFailstatAlways);
+ Call_PushArray(jump, sizeof(jump));
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_JS_InvalidateJump", Native_InvalidateJump);
+}
+
+public int Native_InvalidateJump(Handle plugin, int numParams)
+{
+ InvalidateJumpstat(GetNativeCell(1));
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/commands.sp b/sourcemod/scripting/gokz-jumpstats/commands.sp
new file mode 100644
index 0000000..991f9e6
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/commands.sp
@@ -0,0 +1,28 @@
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_jso", CommandJumpstatsOptions, "[KZ] Open the jumpstats options menu.");
+ RegConsoleCmd("sm_jsalways", CommandAlwaysJumpstats, "[KZ] Toggle the always-on jumpstats.");
+}
+
+public Action CommandJumpstatsOptions(int client, int args)
+{
+ DisplayJumpstatsOptionsMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandAlwaysJumpstats(int client, int args)
+{
+ if (GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Enabled)
+ {
+ GOKZ_JS_SetOption(client, JSOption_JumpstatsAlways, JSToggleOption_Disabled);
+ GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Jumpstats Always - Disable");
+ }
+ else
+ {
+ GOKZ_JS_SetOption(client, JSOption_JumpstatsAlways, JSToggleOption_Enabled);
+ GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Jumpstats Always - Enable");
+ }
+
+ return Plugin_Handled;
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp b/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp
new file mode 100644
index 0000000..3abe8e9
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/distance_tiers.sp
@@ -0,0 +1,118 @@
+/*
+ Categorises jumps into tiers based on their distance.
+ Tier thresholds are loaded from a config.
+*/
+
+
+
+static float distanceTiers[JUMPTYPE_COUNT - 3][MODE_COUNT][DISTANCETIER_COUNT];
+
+
+
+// =====[ PUBLIC ]=====
+
+int GetDistanceTier(int jumpType, int mode, float distance, float offset = 0.0)
+{
+ // No tiers given for 'Invalid' jumps.
+ if (jumpType == JumpType_Invalid || jumpType == JumpType_FullInvalid
+ || jumpType == JumpType_Fall || jumpType == JumpType_Other
+ || jumpType != JumpType_LadderJump && offset < -JS_OFFSET_EPSILON
+ || distance > JS_MAX_JUMP_DISTANCE)
+ {
+ // TODO Give a tier to "Other" jumps
+ // TODO Give a tier to offset jumps
+ return DistanceTier_None;
+ }
+
+ // Get highest tier distance that the jump beats
+ int tier = DistanceTier_None;
+ while (tier + 1 < DISTANCETIER_COUNT && distance >= GetDistanceTierDistance(jumpType, mode, tier + 1))
+ {
+ tier++;
+ }
+
+ return tier;
+}
+
+float GetDistanceTierDistance(int jumpType, int mode, int tier)
+{
+ return distanceTiers[jumpType][mode][tier];
+}
+
+bool LoadBroadcastTiers()
+{
+ char chatTier[16], soundTier[16];
+
+ KeyValues kv = new KeyValues("broadcast");
+ if (!kv.ImportFromFile(JS_CFG_BROADCAST))
+ {
+ return false;
+ }
+
+ kv.GetString("chat", chatTier, sizeof(chatTier), "ownage");
+ kv.GetString("sound", soundTier, sizeof(chatTier), "");
+
+ for (int tier = 0; tier < sizeof(gC_DistanceTierKeys); tier++)
+ {
+ if (StrEqual(chatTier, gC_DistanceTierKeys[tier]))
+ {
+ gI_JSOptionDefaults[JSOption_MinChatBroadcastTier] = tier;
+ }
+ if (StrEqual(soundTier, gC_DistanceTierKeys[tier]))
+ {
+ gI_JSOptionDefaults[JSOption_MinSoundBroadcastTier] = tier;
+ }
+ }
+
+ delete kv;
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_DistanceTiers()
+{
+ if (!LoadDistanceTiers())
+ {
+ SetFailState("Failed to load file: \"%s\".", JS_CFG_TIERS);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static bool LoadDistanceTiers()
+{
+ KeyValues kv = new KeyValues("tiers");
+ if (!kv.ImportFromFile(JS_CFG_TIERS))
+ {
+ return false;
+ }
+
+ // It's a bit of a hack to exclude non-tiered jumptypes
+ for (int jumpType = 0; jumpType < sizeof(gC_JumpTypeKeys) - 3; jumpType++)
+ {
+ if (!kv.JumpToKey(gC_JumpTypeKeys[jumpType]))
+ {
+ return false;
+ }
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ if (!kv.JumpToKey(gC_ModeKeys[mode]))
+ {
+ return false;
+ }
+ for (int tier = DistanceTier_Meh; tier < DISTANCETIER_COUNT; tier++)
+ {
+ distanceTiers[jumpType][mode][tier] = kv.GetFloat(gC_DistanceTierKeys[tier]);
+ }
+ kv.GoBack();
+ }
+ kv.GoBack();
+ }
+ delete kv;
+ return true;
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp b/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp
new file mode 100644
index 0000000..31a1bb2
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/jump_reporting.sp
@@ -0,0 +1,508 @@
+/*
+ Chat and console reports for jumpstats.
+*/
+
+static char sounds[DISTANCETIER_COUNT][256];
+
+
+
+// =====[ PUBLIC ]=====
+
+void PlayJumpstatSound(int client, int tier)
+{
+ int soundOption = GOKZ_JS_GetOption(client, JSOption_MinSoundTier);
+ if (tier <= DistanceTier_Meh || soundOption == DistanceTier_None || soundOption > tier)
+ {
+ return;
+ }
+
+ GOKZ_EmitSoundToClient(client, sounds[tier], _, "Jumpstats");
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_JumpReporting()
+{
+ if (!LoadSounds())
+ {
+ SetFailState("Failed to load file: \"%s\".", JS_CFG_SOUNDS);
+ }
+}
+
+void OnLanding_JumpReporting(Jump jump)
+{
+ int minTier;
+ int tier = GetDistanceTier(jump.type, GOKZ_GetCoreOption(jump.jumper, Option_Mode), jump.distance, jump.offset);
+ if (tier == DistanceTier_None)
+ {
+ return;
+ }
+
+ // Report the jumpstat to the client and their spectators
+ DoJumpstatsReport(jump.jumper, jump, tier);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && client != jump.jumper)
+ {
+ if (GetObserverTarget(client) == jump.jumper)
+ {
+ DoJumpstatsReport(client, jump, tier);
+ }
+ else
+ {
+ minTier = GOKZ_JS_GetOption(client, JSOption_MinChatBroadcastTier);
+ if (minTier != 0 && tier >= minTier)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Broadcast Jumpstat Chat Report",
+ gC_DistanceTierChatColours[tier],
+ jump.jumper,
+ jump.distance,
+ gC_JumpTypes[jump.originalType]);
+ DoConsoleReport(client, false, jump, tier, "Console Jump Header");
+ }
+
+ minTier = GOKZ_JS_GetOption(client, JSOption_MinSoundBroadcastTier);
+ if (minTier != 0 && tier >= minTier)
+ {
+ PlayJumpstatSound(client, tier);
+ }
+ }
+ }
+ }
+}
+
+void OnFailstat_FailstatReporting(Jump jump)
+{
+ int tier = GetDistanceTier(jump.type, GOKZ_GetCoreOption(jump.jumper, Option_Mode), jump.distance);
+ if (tier == DistanceTier_None)
+ {
+ return;
+ }
+
+ // Report the failstat to the client and their spectators
+ DoFailstatReport(jump.jumper, jump, tier);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && GetObserverTarget(client) == jump.jumper)
+ {
+ DoFailstatReport(client, jump, tier);
+ }
+ }
+}
+
+void OnJumpstatAlways_JumpstatAlwaysReporting(Jump jump)
+{
+ DoJumpstatAlwaysReport(jump.jumper, jump);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && GetObserverTarget(client) == jump.jumper)
+ {
+ DoJumpstatAlwaysReport(client, jump);
+ }
+ }
+}
+
+
+void OnFailstatAlways_FailstatAlwaysReporting(Jump jump)
+{
+ DoFailstatAlwaysReport(jump.jumper, jump);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && GetObserverTarget(client) == jump.jumper)
+ {
+ DoFailstatAlwaysReport(client, jump);
+ }
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void DoJumpstatsReport(int client, Jump jump, int tier)
+{
+ if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ DoChatReport(client, false, jump, tier);
+ DoConsoleReport(client, false, jump, tier, "Console Jump Header");
+ PlayJumpstatSound(client, tier);
+}
+
+static void DoFailstatReport(int client, Jump jump, int tier)
+{
+ if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ DoChatReport(client, true, jump, tier);
+ DoConsoleReport(client, true, jump, tier, "Console Failstat Header");
+}
+
+static void DoJumpstatAlwaysReport(int client, Jump jump)
+{
+ if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled ||
+ GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ DoChatReport(client, false, jump, 1);
+ DoConsoleReport(client, false, jump, 1, "Console Jump Header");
+}
+
+static void DoFailstatAlwaysReport(int client, Jump jump)
+{
+ if (GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled ||
+ GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ DoChatReport(client, true, jump, 1);
+ DoConsoleReport(client, true, jump, 1, "Console Failstat Header");
+}
+
+
+
+
+// CONSOLE REPORT
+
+static void DoConsoleReport(int client, bool isFailstat, Jump jump, int tier, char[] header)
+{
+ int minConsoleTier = GOKZ_JS_GetOption(client, JSOption_MinConsoleTier);
+ if ((minConsoleTier == 0 || minConsoleTier > tier) && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled
+ || isFailstat && GOKZ_JS_GetOption(client, JSOption_FailstatsConsole) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ char releaseWString[32], blockString[32], edgeString[32], deviationString[32], missString[32];
+
+ if (jump.originalType == JumpType_LongJump ||
+ jump.originalType == JumpType_LadderJump ||
+ jump.originalType == JumpType_WeirdJump ||
+ jump.originalType == JumpType_LowpreWeirdJump)
+ {
+ FormatEx(releaseWString, sizeof(releaseWString), " %s", GetIntConsoleString(client, "W Release", jump.releaseW));
+ }
+ else if (jump.crouchRelease < 20 && jump.crouchRelease > -20)
+ {
+ FormatEx(releaseWString, sizeof(releaseWString), " %s", GetIntConsoleString(client, "Crouch Release", jump.crouchRelease));
+ }
+
+ if (jump.miss > 0.0)
+ {
+ FormatEx(missString, sizeof(missString), " %s", GetFloatConsoleString2(client, "Miss", jump.miss));
+ }
+
+ if (jump.block > 0)
+ {
+ FormatEx(blockString, sizeof(blockString), " %s", GetIntConsoleString(client, "Block", jump.block));
+ FormatEx(deviationString, sizeof(deviationString), " %s", GetFloatConsoleString1(client, "Deviation", jump.deviation));
+ }
+
+ if (jump.edge > 0.0 || (jump.block > 0 && jump.edge == 0.0))
+ {
+ FormatEx(edgeString, sizeof(edgeString), " %s", GetFloatConsoleString2(client, "Edge", jump.edge));
+ }
+
+ PrintToConsole(client, "%t", header, jump.jumper, jump.distance, gC_JumpTypes[jump.originalType]);
+
+ PrintToConsole(client, "%s%s%s%s %s %s %s %s%s %s %s%s %s %s %s %s %s",
+ gC_ModeNamesShort[GOKZ_GetCoreOption(jump.jumper, Option_Mode)],
+ blockString,
+ edgeString,
+ missString,
+ GetIntConsoleString(client, jump.strafes == 1 ? "Strafe" : "Strafes", jump.strafes),
+ GetSyncConsoleString(client, jump.sync),
+ GetFloatConsoleString2(client, "Pre", jump.preSpeed),
+ GetFloatConsoleString2(client, "Max", jump.maxSpeed),
+ releaseWString,
+ GetIntConsoleString(client, "Overlap", jump.overlap),
+ GetIntConsoleString(client, "Dead Air", jump.deadair),
+ deviationString,
+ GetWidthConsoleString(client, jump.width, jump.strafes),
+ GetFloatConsoleString1(client, "Height", jump.height),
+ GetIntConsoleString(client, "Airtime", jump.duration),
+ GetFloatConsoleString1(client, "Offset", jump.offset),
+ GetIntConsoleString(client, "Crouch Ticks", jump.crouchTicks));
+
+ PrintToConsole(client, " #. %12t%12t%12t%12t%12t%9t%t", "Sync (Table)", "Gain (Table)", "Loss (Table)", "Airtime (Table)", "Width (Table)", "Overlap (Table)", "Dead Air (Table)");
+ if (jump.strafes_ticks[0] > 0)
+ {
+ PrintToConsole(client, " 0. ---- ----- ----- %3.0f%% ----- -- --", GetStrafeAirtime(jump, 0));
+ }
+ for (int strafe = 1; strafe <= jump.strafes && strafe < JS_MAX_TRACKED_STRAFES; strafe++)
+ {
+ PrintToConsole(client,
+ " %2d. %3.0f%% %5.2f %5.2f %3.0f%% %5.1f° %2d %2d",
+ strafe,
+ GetStrafeSync(jump, strafe),
+ jump.strafes_gain[strafe],
+ jump.strafes_loss[strafe],
+ GetStrafeAirtime(jump, strafe),
+ FloatAbs(jump.strafes_width[strafe]),
+ jump.strafes_overlap[strafe],
+ jump.strafes_deadair[strafe]);
+ }
+ PrintToConsole(client, ""); // New line
+}
+
+static char[] GetSyncConsoleString(int client, float sync)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString), "| %.0f%% %T", sync, "Sync", client);
+ return resultString;
+}
+
+static char[] GetWidthConsoleString(int client, float width, int strafes)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString), "| %.1f° %T", GetAverageStrafeWidth(strafes, width), "Width", client);
+ return resultString;
+}
+
+// I couldn't really merge those together
+static char[] GetFloatConsoleString1(int client, const char[] stat, float value)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString), "| %.1f %T", value, stat, client);
+ return resultString;
+}
+
+static char[] GetFloatConsoleString2(int client, const char[] stat, float value)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString), "| %.2f %T", value, stat, client);
+ return resultString;
+}
+
+static char[] GetIntConsoleString(int client, const char[] stat, int value)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString), "| %d %T", value, stat, client);
+ return resultString;
+}
+
+
+
+// CHAT REPORT
+
+static void DoChatReport(int client, bool isFailstat, Jump jump, int tier)
+{
+ int minChatTier = GOKZ_JS_GetOption(client, JSOption_MinChatTier);
+ if ((minChatTier == 0 || minChatTier > tier) // 0 means disabled
+ && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ char typePostfix[3], color[16], blockStats[32], extBlockStats[32];
+ char releaseStats[32], edgeOffset[64], offsetEdge[32], missString[32];
+
+ if (isFailstat)
+ {
+ if (GOKZ_JS_GetOption(client, JSOption_FailstatsChat) == JSToggleOption_Disabled
+ && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+ strcopy(typePostfix, sizeof(typePostfix), "-F");
+ strcopy(color, sizeof(color), "{grey}");
+ }
+ else
+ {
+ strcopy(color, sizeof(color), gC_DistanceTierChatColours[tier]);
+ }
+
+ if (jump.block > 0)
+ {
+ FormatEx(blockStats, sizeof(blockStats), " | %s", GetFloatChatString(client, "Edge", jump.edge));
+ FormatEx(extBlockStats, sizeof(extBlockStats), " | %s", GetFloatChatString(client, "Deviation", jump.deviation));
+ }
+
+ if (jump.miss > 0.0)
+ {
+ FormatEx(missString, sizeof(missString), " | %s", GetFloatChatString(client, "Miss", jump.miss));
+ }
+
+ if (jump.edge > 0.0 || (jump.block > 0 && jump.edge == 0.0))
+ {
+ if (jump.originalType == JumpType_LadderJump)
+ {
+ FormatEx(offsetEdge, sizeof(offsetEdge), " | %s", GetFloatChatString(client, "Edge", jump.edge));
+ }
+ else
+ {
+ FormatEx(edgeOffset, sizeof(edgeOffset), " | %s", GetFloatChatString(client, "Edge", jump.edge));
+ }
+ }
+
+ if (jump.originalType == JumpType_LongJump ||
+ jump.originalType == JumpType_LadderJump ||
+ jump.originalType == JumpType_WeirdJump)
+ {
+ if (jump.releaseW >= 20 || jump.releaseW <= -20)
+ {
+ FormatEx(releaseStats, sizeof(releaseStats), " | {red}✗ {grey}W", GetReleaseChatString(client, "W Release", jump.releaseW));
+ }
+ else
+ {
+ FormatEx(releaseStats, sizeof(releaseStats), " | %s", GetReleaseChatString(client, "W Release", jump.releaseW));
+ }
+ }
+ else if (jump.crouchRelease < 20 && jump.crouchRelease > -20)
+ {
+ FormatEx(releaseStats, sizeof(releaseStats), " | %s", GetReleaseChatString(client, "Crouch Release", jump.crouchRelease));
+ }
+
+ if (jump.originalType == JumpType_LadderJump)
+ {
+ FormatEx(edgeOffset, sizeof(edgeOffset), " | %s", GetFloatChatString(client, "Offset Short", jump.offset));
+ }
+ else
+ {
+ FormatEx(offsetEdge, sizeof(offsetEdge), " | %s", GetFloatChatString(client, "Offset", jump.offset));
+ }
+
+ GOKZ_PrintToChat(client, true,
+ "%s%s%s{grey}: %s%.1f{grey} | %s | %s%s%s",
+ color,
+ gC_JumpTypesShort[jump.originalType],
+ typePostfix,
+ color,
+ jump.distance,
+ GetStrafesSyncChatString(client, jump.strafes, jump.sync),
+ GetSpeedChatString(client, jump.preSpeed, jump.maxSpeed),
+ edgeOffset,
+ releaseStats);
+
+ if (GOKZ_JS_GetOption(client, JSOption_ExtendedChatReport) == JSToggleOption_Enabled)
+ {
+ GOKZ_PrintToChat(client, false,
+ "%s | %s%s%s | %s | %s%s",
+ GetIntChatString(client, "Overlap", jump.overlap),
+ GetIntChatString(client, "Dead Air", jump.deadair),
+ offsetEdge,
+ extBlockStats,
+ GetWidthChatString(client, jump.width, jump.strafes),
+ GetFloatChatString(client, "Height", jump.height),
+ missString);
+ }
+}
+
+static char[] GetStrafesSyncChatString(int client, int strafes, float sync)
+{
+ char resultString[64];
+ FormatEx(resultString, sizeof(resultString),
+ "{lime}%d{grey} %T ({lime}%.0f%%%%{grey})",
+ strafes, "Strafes", client, sync);
+ return resultString;
+}
+
+static char[] GetSpeedChatString(int client, float preSpeed, float maxSpeed)
+{
+ char resultString[64];
+ FormatEx(resultString, sizeof(resultString),
+ "{lime}%.0f{grey} / {lime}%.0f{grey} %T",
+ preSpeed, maxSpeed, "Speed", client);
+ return resultString;
+}
+
+static char[] GetReleaseChatString(int client, char[] releaseType, int release)
+{
+ char resultString[32];
+ if (release == 0)
+ {
+ FormatEx(resultString, sizeof(resultString),
+ "{green}✓{grey} %T",
+ releaseType, client);
+ }
+ else if (release > 0)
+ {
+ FormatEx(resultString, sizeof(resultString),
+ "{red}+%d{grey} %T",
+ release,
+ releaseType, client);
+ }
+ else
+ {
+ FormatEx(resultString, sizeof(resultString),
+ "{blue}%d{grey} %T",
+ release,
+ releaseType, client);
+ }
+ return resultString;
+}
+
+static char[] GetWidthChatString(int client, float width, int strafes)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString),
+ "{lime}%.1f°{grey} %T",
+ GetAverageStrafeWidth(strafes, width), "Width", client);
+ return resultString;
+}
+
+static float GetAverageStrafeWidth(int strafes, float totalWidth)
+{
+ if (strafes == 0)
+ {
+ return 0.0;
+ }
+
+ return totalWidth / strafes;
+}
+
+static char[] GetFloatChatString(int client, const char[] stat, float value)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString),
+ "{lime}%.1f{grey} %T",
+ value, stat, client);
+ return resultString;
+}
+
+static char[] GetIntChatString(int client, const char[] stat, int value)
+{
+ char resultString[32];
+ FormatEx(resultString, sizeof(resultString),
+ "{lime}%d{grey} %T",
+ value, stat, client);
+ return resultString;
+}
+
+
+
+// SOUNDS
+
+static bool LoadSounds()
+{
+ KeyValues kv = new KeyValues("sounds");
+ if (!kv.ImportFromFile(JS_CFG_SOUNDS))
+ {
+ return false;
+ }
+
+ char downloadPath[256];
+ for (int tier = DistanceTier_Impressive; tier < DISTANCETIER_COUNT; tier++)
+ {
+ kv.GetString(gC_DistanceTierKeys[tier], sounds[tier], sizeof(sounds[]));
+ FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", sounds[tier]);
+ AddFileToDownloadsTable(downloadPath);
+ PrecacheSound(sounds[tier], true);
+ }
+
+ delete kv;
+ return true;
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp b/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp
new file mode 100644
index 0000000..acd9442
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp
@@ -0,0 +1,1624 @@
+/*
+ Tracking of jump type, speed, strafes and more.
+*/
+
+
+
+// =====[ STRUCTS ]============================================================
+
+enum struct Pose
+{
+ float position[3];
+ float orientation[3];
+ float velocity[3];
+ float speed;
+ int duration;
+ int overlap;
+ int deadair;
+ int syncTicks;
+}
+
+
+
+// =====[ GLOBAL VARIABLES ]===================================================
+
+static ArrayList entityTouchList[MAXPLAYERS + 1];
+static int entityTouchDuration[MAXPLAYERS + 1];
+static int lastNoclipTime[MAXPLAYERS + 1];
+static int lastDuckbugTime[MAXPLAYERS + 1];
+static int lastGroundSpeedCappedTime[MAXPLAYERS + 1];
+static int lastMovementProcessedTime[MAXPLAYERS + 1];
+static float lastJumpButtonTime[MAXPLAYERS + 1];
+static bool validCmd[MAXPLAYERS + 1]; // Whether no illegal action is detected
+static const float playerMins[3] = { -16.0, -16.0, 0.0 };
+static const float playerMaxs[3] = { 16.0, 16.0, 0.0 };
+static const float playerMinsEx[3] = { -20.0, -20.0, 0.0 };
+static const float playerMaxsEx[3] = { 20.0, 20.0, 0.0 };
+static bool doFailstatAlways[MAXPLAYERS + 1];
+static bool isInAir[MAXPLAYERS + 1];
+static const Jump emptyJump;
+static Handle acceptInputHook;
+
+
+// =====[ DEFINITIONS ]========================================================
+
+// We cannot return enum structs and it's annoying
+// The modulo operator is broken, so we can't access this using negative numbers
+// (https://github.com/alliedmodders/sourcepawn/issues/456). We use the method
+// described here instead: https://stackoverflow.com/a/42131603/7421666
+#define pose(%1) (poseHistory[this.jumper][((this.poseIndex + (%1)) % JS_FAILSTATS_MAX_TRACKED_TICKS + JS_FAILSTATS_MAX_TRACKED_TICKS) % JS_FAILSTATS_MAX_TRACKED_TICKS])
+
+
+
+// =====[ TRACKING ]===========================================================
+
+// We cannot put that into the tracker struct
+Pose poseHistory[MAXPLAYERS + 1][JS_FAILSTATS_MAX_TRACKED_TICKS];
+
+enum struct JumpTracker
+{
+ Jump jump;
+ int jumper;
+ int jumpoffTick;
+ int poseIndex;
+ int strafeDirection;
+ int lastJumpTick;
+ int lastTeleportTick;
+ int lastType;
+ int lastWPressedTick;
+ int nextCrouchRelease;
+ int syncTicks;
+ int lastCrouchPressedTick;
+ int tickCount;
+ bool failstatBlockDetected;
+ bool failstatFailed;
+ bool failstatValid;
+ float failstatBlockHeight;
+ float takeoffOrigin[3];
+ float takeoffVelocity[3];
+ float position[3];
+
+ void Init(int jumper)
+ {
+ this.jumper = jumper;
+ this.jump.jumper = jumper;
+ this.nextCrouchRelease = 100;
+ this.tickCount = 0;
+ }
+
+
+
+ // =====[ ENTRYPOINTS ]=======================================================
+
+ void Reset(bool jumped, bool ladderJump, bool jumpbug)
+ {
+ // We need to do that before we reset the jump cause we need the
+ // offset and type of the previous jump
+ this.lastType = this.DetermineType(jumped, ladderJump, jumpbug);
+
+ // We need this for weirdjump w-release
+ int releaseWTemp = this.jump.releaseW;
+
+ // Reset all stats
+ this.jump = emptyJump;
+ this.jump.type = this.lastType;
+ this.jump.jumper = this.jumper;
+ this.syncTicks = 0;
+ this.strafeDirection = StrafeDirection_None;
+ this.jump.releaseW = 100;
+
+ // We have to show this on the jumpbug stat, not the lj stat
+ this.jump.crouchRelease = this.nextCrouchRelease;
+ this.nextCrouchRelease = 100;
+
+ // Handle weirdjump w-release
+ if (this.jump.type == JumpType_WeirdJump)
+ {
+ this.jump.releaseW = releaseWTemp;
+ }
+
+ // Reset pose history
+ this.poseIndex = 0;
+ // Update the first tick if it is a jumpbug.
+ this.UpdateOnGround();
+ }
+
+ void Begin()
+ {
+ // Initialize stats
+ this.CalcTakeoff();
+ this.AdjustLowpreJumptypes();
+
+ this.failstatBlockDetected = this.jump.type != JumpType_LadderJump;
+ this.failstatFailed = false;
+ this.failstatValid = false;
+ this.failstatBlockHeight = this.takeoffOrigin[2];
+
+ // Store the original type for the always stats
+ this.jump.originalType = this.jump.type;
+
+ // Notify everyone about the takeoff
+ Call_OnTakeoff(this.jumper, this.jump.type);
+ }
+
+ void Update()
+ {
+ this.UpdatePoseHistory();
+
+ float speed = pose(0).speed;
+
+ // Fix certain props that don't give you base velocity
+ /*
+ We check for speed reduction for abuse; while prop abuses increase speed,
+ wall collision will very likely (if not always) result in a speed reduction.
+ */
+ float actualSpeed = GetVectorHorizontalDistance(this.position, pose(-1).position) / GetTickInterval();
+ if (FloatAbs(speed - actualSpeed) > JS_SPEED_MODIFICATION_TOLERANCE && this.jump.duration != 0)
+ {
+ if (actualSpeed <= pose(-1).speed)
+ {
+ pose(0).speed = actualSpeed;
+ }
+ // This check is needed if you land via ducking instead of moving (duckbug)
+ else if (FloatAbs(actualSpeed) > EPSILON)
+ {
+ this.Invalidate();
+ }
+ }
+ // You shouldn't gain any vertical velocity during a jump.
+ // This would only happen if you get boosted back up somehow, or you edgebugged.
+ if (!Movement_GetOnGround(this.jumper) && pose(0).velocity[2] > pose(-1).velocity[2])
+ {
+ this.Invalidate();
+ }
+
+ this.jump.height = FloatMax(this.jump.height, this.position[2] - this.takeoffOrigin[2]);
+ this.jump.maxSpeed = FloatMax(this.jump.maxSpeed, speed);
+ this.jump.crouchTicks += Movement_GetDucking(this.jumper) ? 1 : 0;
+ this.syncTicks += speed > pose(-1).speed ? 1 : 0;
+ this.jump.duration++;
+
+ this.UpdateStrafes();
+ this.UpdateFailstat();
+ this.UpdatePoseStats();
+
+ this.lastType = this.jump.type;
+ }
+
+ void End()
+ {
+ // The jump is so invalid we don't even have to bother.
+ // Also check if the player just teleported.
+ if (this.jump.type == JumpType_FullInvalid ||
+ this.tickCount - this.lastTeleportTick < JS_MIN_TELEPORT_DELAY)
+ {
+ return;
+ }
+
+ // Measure last tick of jumpstat
+ this.Update();
+
+ // Fix the edgebug for the current position
+ Movement_GetNobugLandingOrigin(this.jumper, this.position);
+
+ // There are a couple bugs and exploits we have to check for
+ this.EndBugfixExploits();
+
+ // Calculate the last stats
+ this.jump.distance = this.CalcDistance();
+ this.jump.sync = float(this.syncTicks) / float(this.jump.duration) * 100.0;
+ this.jump.offset = this.position[2] - this.takeoffOrigin[2];
+
+ this.EndBlockDistance();
+
+ // Make sure the ladder has no offset for ladder jumps
+ if (this.jump.type == JumpType_LadderJump)
+ {
+ this.TraceLadderOffset(this.position[2]);
+ }
+
+ // Calculate always-on stats
+ if (GOKZ_JS_GetOption(this.jumper, JSOption_JumpstatsAlways) == JSToggleOption_Enabled)
+ {
+ this.EndAlwaysJumpstats();
+ }
+
+ // Call the appropriate functions for either regular or always stats
+ this.Callback();
+ }
+
+ void Invalidate()
+ {
+ if (this.jump.type != JumpType_Invalid &&
+ this.jump.type != JumpType_FullInvalid)
+ {
+ this.jump.type = JumpType_Invalid;
+ Call_OnJumpInvalidated(this.jumper);
+ }
+ }
+
+
+
+ // =====[ BEGIN HELPERS ]=====================================================
+
+ void CalcTakeoff()
+ {
+ // MovementAPI now correctly calculates the takeoff origin
+ // and velocity for jumpbugs. What is wrong though, is how
+ // mode plugins set bhop prespeed.
+ // Jumpbug takeoff origin is correct.
+ Movement_GetTakeoffOrigin(this.jumper, this.takeoffOrigin);
+ Movement_GetTakeoffVelocity(this.jumper, this.takeoffVelocity);
+ if (this.jump.type == JumpType_Jumpbug || this.jump.type == JumpType_MultiBhop
+ || this.jump.type == JumpType_Bhop || this.jump.type == JumpType_LowpreBhop
+ || this.jump.type == JumpType_LowpreWeirdJump || this.jump.type == JumpType_WeirdJump)
+ {
+ // Move the origin to the ground.
+ // The difference can only be 2 units maximum.
+ float bhopOrigin[3];
+ CopyVector(this.takeoffOrigin, bhopOrigin);
+ bhopOrigin[2] -= 2.0;
+ TraceHullPosition(this.takeoffOrigin, bhopOrigin, playerMins, playerMaxs, this.takeoffOrigin);
+ }
+
+ this.jump.preSpeed = Movement_GetTakeoffSpeed(this.jumper);
+ poseHistory[this.jumper][0].speed = this.jump.preSpeed;
+ }
+
+ void AdjustLowpreJumptypes()
+ {
+ // Exclude SKZ and VNL stats.
+ if (GOKZ_GetCoreOption(this.jumper, Option_Mode) == Mode_KZTimer)
+ {
+ if (this.jump.type == JumpType_Bhop &&
+ this.jump.preSpeed < 360.0)
+ {
+ this.jump.type = JumpType_LowpreBhop;
+ }
+ else if (this.jump.type == JumpType_WeirdJump &&
+ this.jump.preSpeed < 300.0)
+ {
+ this.jump.type = JumpType_LowpreWeirdJump;
+ }
+ }
+ }
+
+ int DetermineType(bool jumped, bool ladderJump, bool jumpbug)
+ {
+ if (gB_SpeedJustModifiedExternally[this.jumper] || this.tickCount - this.lastTeleportTick < JS_MIN_TELEPORT_DELAY)
+ {
+ return JumpType_Invalid;
+ }
+ else if (ladderJump)
+ {
+ // Check for ladder gliding.
+ float curtime = GetGameTime();
+ float ignoreLadderJumpTime = GetEntPropFloat(this.jumper, Prop_Data, "m_ignoreLadderJumpTime");
+ // Check if the ladder glide period is still active and if the player held jump in that period.
+ if (ignoreLadderJumpTime > curtime &&
+ ignoreLadderJumpTime - IGNORE_JUMP_TIME < lastJumpButtonTime[this.jumper] && lastJumpButtonTime[this.jumper] < ignoreLadderJumpTime)
+ {
+ return JumpType_Invalid;
+ }
+ if (jumped)
+ {
+ return JumpType_Ladderhop;
+ }
+ else
+ {
+ return JumpType_LadderJump;
+ }
+ }
+ else if (!jumped)
+ {
+ return JumpType_Fall;
+ }
+ else if (jumpbug)
+ {
+ // Check for no offset
+ // The origin and offset is now correct, no workaround needed
+ if (FloatAbs(this.jump.offset) < JS_OFFSET_EPSILON && this.lastType == JumpType_LongJump)
+ {
+ return JumpType_Jumpbug;
+ }
+ else
+ {
+ return JumpType_Invalid;
+ }
+ }
+ else if (this.HitBhop() && !this.HitDuckbugRecently())
+ {
+ // Check for no offset
+ if (FloatAbs(this.jump.offset) < JS_OFFSET_EPSILON)
+ {
+ switch (this.lastType)
+ {
+ case JumpType_LongJump:return JumpType_Bhop;
+ case JumpType_Bhop:return JumpType_MultiBhop;
+ case JumpType_LowpreBhop:return JumpType_MultiBhop;
+ case JumpType_MultiBhop:return JumpType_MultiBhop;
+ default:return JumpType_Other;
+ }
+ }
+ // Check for weird jump
+ else if (this.lastType == JumpType_Fall &&
+ this.ValidWeirdJumpDropDistance())
+ {
+ return JumpType_WeirdJump;
+ }
+ else
+ {
+ return JumpType_Other;
+ }
+ }
+ if (this.HitDuckbugRecently() || !this.GroundSpeedCappedRecently())
+ {
+ return JumpType_Invalid;
+ }
+ return JumpType_LongJump;
+ }
+
+ bool HitBhop()
+ {
+ return Movement_GetTakeoffCmdNum(this.jumper) - Movement_GetLandingCmdNum(this.jumper) <= JS_MAX_BHOP_GROUND_TICKS;
+ }
+
+ bool ValidWeirdJumpDropDistance()
+ {
+ if (this.jump.offset < -1 * JS_MAX_WEIRDJUMP_FALL_OFFSET)
+ {
+ // Don't bother telling them if they fell a very far distance
+ if (!GetJumpstatsDisabled(this.jumper) && this.jump.offset >= -2 * JS_MAX_WEIRDJUMP_FALL_OFFSET)
+ {
+ GOKZ_PrintToChat(this.jumper, true, "%t", "Dropped Too Far (Weird Jump)", -1 * this.jump.offset, JS_MAX_WEIRDJUMP_FALL_OFFSET);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ bool HitDuckbugRecently()
+ {
+ return this.tickCount - lastDuckbugTime[this.jumper] <= JS_MAX_DUCKBUG_RESET_TICKS;
+ }
+
+ bool GroundSpeedCappedRecently()
+ {
+ // A valid longjump needs to have their ground speed capped the tick right before.
+ return lastGroundSpeedCappedTime[this.jumper] == lastMovementProcessedTime[this.jumper];
+ }
+
+ // =====[ UPDATE HELPERS ]====================================================
+
+ // We split that up in two functions to get a reference to the pose so we
+ // don't have to recalculate the pose index all the time.
+ void UpdatePoseHistory()
+ {
+ this.poseIndex++;
+ this.UpdatePose(pose(0));
+ }
+
+ void UpdatePose(Pose p)
+ {
+ Movement_GetProcessingOrigin(this.jumper, p.position);
+ Movement_GetProcessingVelocity(this.jumper, p.velocity);
+ Movement_GetEyeAngles(this.jumper, p.orientation);
+ p.speed = GetVectorHorizontalLength(p.velocity);
+
+ // We use the current position in a lot of places, so we store it
+ // separately to avoid calling 'pose' all the time.
+ CopyVector(p.position, this.position);
+ }
+
+ // We split that up in two functions to get a reference to the pose so we
+ // don't have to recalculate the pose index all the time. We seperate that
+ // from UpdatePose() cause those stats are not calculated yet when we call that.
+ void UpdatePoseStats()
+ {
+ this.UpdatePoseStats_P(pose(0));
+ }
+
+ void UpdatePoseStats_P(Pose p)
+ {
+ p.duration = this.jump.duration;
+ p.syncTicks = this.syncTicks;
+ p.overlap = this.jump.overlap;
+ p.deadair = this.jump.deadair;
+ }
+
+ void UpdateOnGround()
+ {
+ // We want accurate values to measure the first tick
+ this.UpdatePose(poseHistory[this.jumper][0]);
+ }
+
+ void UpdateRelease()
+ {
+ // Using UpdateOnGround doesn't work because
+ // takeoff tick is calculated after leaving the ground.
+ this.jumpoffTick = Movement_GetTakeoffTick(this.jumper);
+
+ // We also check IN_BACK cause that happens for backwards ladderjumps
+ if (Movement_GetButtons(this.jumper) & IN_FORWARD ||
+ Movement_GetButtons(this.jumper) & IN_BACK)
+ {
+ this.lastWPressedTick = this.tickCount;
+ }
+ else if (this.jump.releaseW > 99)
+ {
+ this.jump.releaseW = this.lastWPressedTick - this.jumpoffTick + 1;
+ }
+
+ if (Movement_GetButtons(this.jumper) & IN_DUCK)
+ {
+ this.lastCrouchPressedTick = this.tickCount;
+ this.nextCrouchRelease = 100;
+ }
+ else if (this.nextCrouchRelease > 99)
+ {
+ this.nextCrouchRelease = this.lastCrouchPressedTick - this.jumpoffTick - 95;
+ }
+ }
+
+ void UpdateStrafes()
+ {
+ // Strafe direction
+ if (Movement_GetTurningLeft(this.jumper) &&
+ this.strafeDirection != StrafeDirection_Left)
+ {
+ this.strafeDirection = StrafeDirection_Left;
+ this.jump.strafes++;
+ }
+ else if (Movement_GetTurningRight(this.jumper) &&
+ this.strafeDirection != StrafeDirection_Right)
+ {
+ this.strafeDirection = StrafeDirection_Right;
+ this.jump.strafes++;
+ }
+
+ // Overlap / Deadair
+ int buttons = Movement_GetButtons(this.jumper);
+ int overlap = buttons & IN_MOVERIGHT && buttons & IN_MOVELEFT ? 1 : 0;
+ int deadair = !(buttons & IN_MOVERIGHT) && !(buttons & IN_MOVELEFT) ? 1 : 0;
+
+ // Sync / Gain / Loss
+ float deltaSpeed = pose(0).speed - pose(-1).speed;
+ bool gained = deltaSpeed > EPSILON;
+ bool lost = deltaSpeed < -EPSILON;
+
+ // Width
+ float width = FloatAbs(CalcDeltaAngle(pose(0).orientation[1], pose(-1).orientation[1]));
+
+ // Overall stats
+ this.jump.overlap += overlap;
+ this.jump.deadair += deadair;
+ this.jump.width += width;
+
+ // Individual stats
+ if (this.jump.strafes >= JS_MAX_TRACKED_STRAFES)
+ {
+ return;
+ }
+
+ int i = this.jump.strafes;
+ this.jump.strafes_ticks[i]++;
+
+ this.jump.strafes_overlap[i] += overlap;
+ this.jump.strafes_deadair[i] += deadair;
+ this.jump.strafes_loss[i] += lost ? -1 * deltaSpeed : 0.0;
+ this.jump.strafes_width[i] += width;
+
+ if (gained)
+ {
+ this.jump.strafes_gainTicks[i]++;
+ this.jump.strafes_gain[i] += deltaSpeed;
+ }
+ }
+
+ void UpdateFailstat()
+ {
+ int coordDist, distSign;
+ float failstatPosition[3], block[3], traceStart[3];
+
+ // There's no point in going further if we're already done
+ if (this.failstatValid || this.failstatFailed)
+ {
+ return;
+ }
+
+ // Get the coordinate system orientation.
+ GetCoordOrientation(this.position, this.takeoffOrigin, coordDist, distSign);
+
+ // For ladderjumps we have to find the landing block early so we know at which point the jump failed.
+ // For this, we search for the block 10 units above the takeoff origin, assuming the player already
+ // traveled a significant enough distance in the direction of the block at this time.
+ if (!this.failstatBlockDetected &&
+ this.position[2] - this.takeoffOrigin[2] < 10.0 &&
+ this.jump.height > 10.0)
+ {
+ this.failstatBlockDetected = true;
+
+ // Setup a trace to search for the block
+ CopyVector(this.takeoffOrigin, traceStart);
+ traceStart[2] -= 5.0;
+ CopyVector(traceStart, block);
+ traceStart[coordDist] += JS_MIN_LAJ_BLOCK_DISTANCE * distSign;
+ block[coordDist] += JS_MAX_LAJ_FAILSTAT_DISTANCE * distSign;
+
+ // Search for the block
+ if (!TraceHullPosition(traceStart, block, playerMins, playerMaxs, block))
+ {
+ // Mark the calculation as failed
+ this.failstatFailed = true;
+ return;
+ }
+
+ // Find the block height
+ block[2] += 5.0;
+ this.failstatBlockHeight = this.FindBlockHeight(block, float(distSign) * 17.0, coordDist, 10.0) - 0.031250;
+ }
+
+ // Only do the calculation once we're below the block level
+ if (this.position[2] >= this.failstatBlockHeight)
+ {
+ // We need that cause we can duck after getting lower than the failstat
+ // height and still make the block.
+ this.failstatValid = false;
+ return;
+ }
+
+ // Calculate the true origin where the player would have hit the ground.
+ this.GetFailOrigin(this.failstatBlockHeight, failstatPosition, -1);
+
+ // Calculate the jump distance.
+ this.jump.distance = FloatAbs(GetVectorHorizontalDistance(failstatPosition, this.takeoffOrigin));
+
+ // Construct the maximum landing origin, assuming the player reached
+ // at least the middle of the gap.
+ CopyVector(this.takeoffOrigin, block);
+ block[coordDist] = 2 * failstatPosition[coordDist] - this.takeoffOrigin[coordDist];
+ block[view_as<int>(!coordDist)] = failstatPosition[view_as<int>(!coordDist)];
+ block[2] = this.failstatBlockHeight;
+
+ // Calculate block stats
+ if ((this.lastType == JumpType_LongJump ||
+ this.lastType == JumpType_Bhop ||
+ this.lastType == JumpType_MultiBhop ||
+ this.lastType == JumpType_Ladderhop ||
+ this.lastType == JumpType_WeirdJump ||
+ this.lastType == JumpType_Jumpbug ||
+ this.lastType == JumpType_LowpreBhop ||
+ this.lastType == JumpType_LowpreWeirdJump)
+ && this.jump.distance >= JS_MIN_BLOCK_DISTANCE)
+ {
+ // Add the player model to the distance.
+ this.jump.distance += 32.0;
+
+ this.CalcBlockStats(block, true);
+ }
+ else if (this.lastType == JumpType_LadderJump &&
+ this.jump.distance >= JS_MIN_LAJ_BLOCK_DISTANCE)
+ {
+ this.CalcLadderBlockStats(block, true);
+ }
+ else
+ {
+ this.failstatFailed = true;
+ return;
+ }
+
+ if (this.jump.block > 0)
+ {
+ // Calculate the last stats
+ this.jump.sync = float(this.syncTicks) / float(this.jump.duration) * 100.0;
+ this.jump.offset = failstatPosition[2] - this.takeoffOrigin[2];
+
+ // Call the callback for the reporting.
+ Call_OnFailstat(this.jump);
+
+ // Mark the calculation as successful
+ this.failstatValid = true;
+ }
+ else
+ {
+ this.failstatFailed = true;
+ }
+ }
+
+
+
+ // =====[ END HELPERS ]=====================================================
+
+ float CalcDistance()
+ {
+ float distance = GetVectorHorizontalDistance(this.takeoffOrigin, this.position);
+
+ // Check whether the distance is NaN
+ if (distance != distance)
+ {
+ this.Invalidate();
+
+ // We need that for the always stats
+ float pos[3];
+
+ // For the always stats it's ok to ignore the bug
+ Movement_GetOrigin(this.jumper, pos);
+
+ distance = GetVectorHorizontalDistance(this.takeoffOrigin, pos);
+ }
+
+ if (this.jump.originalType != JumpType_LadderJump)
+ {
+ distance += 32.0;
+ }
+ return distance;
+ }
+
+ void EndBlockDistance()
+ {
+ if ((this.jump.type == JumpType_LongJump ||
+ this.jump.type == JumpType_Bhop ||
+ this.jump.type == JumpType_MultiBhop ||
+ this.jump.type == JumpType_Ladderhop ||
+ this.jump.type == JumpType_WeirdJump ||
+ this.jump.type == JumpType_Jumpbug ||
+ this.jump.type == JumpType_LowpreBhop ||
+ this.jump.type == JumpType_LowpreWeirdJump)
+ && this.jump.distance >= JS_MIN_BLOCK_DISTANCE)
+ {
+ this.CalcBlockStats(this.position);
+ }
+ else if (this.jump.type == JumpType_LadderJump &&
+ this.jump.distance >= JS_MIN_LAJ_BLOCK_DISTANCE)
+ {
+ this.CalcLadderBlockStats(this.position);
+ }
+ }
+
+ void EndAlwaysJumpstats()
+ {
+ // Only calculate that form of edge if the regular block calculations failed
+ if (this.jump.block == 0 && this.jump.type != JumpType_LadderJump)
+ {
+ this.CalcAlwaysEdge();
+ }
+
+ // It's possible that the offset calculation failed with the nobug origin
+ // functions, so we have to fix it when that happens. The offset shouldn't
+ // be affected by the bug anyway.
+ if (this.jump.offset != this.jump.offset)
+ {
+ Movement_GetOrigin(this.jumper, this.position);
+ this.jump.offset = this.position[2] - this.takeoffOrigin[2];
+ }
+ }
+
+ void EndBugfixExploits()
+ {
+ // Try to prevent a form of booster abuse
+ if (!this.IsValidAirtime())
+ {
+ this.Invalidate();
+ }
+ }
+
+ bool IsValidAirtime()
+ {
+ // Ladderjumps can have pretty much any airtime.
+ if (this.jump.type == JumpType_LadderJump)
+ {
+ return true;
+ }
+
+ // Ladderhops can have a maximum airtime of 102.
+ if (this.jump.type == JumpType_Ladderhop
+ && this.jump.duration <= 102)
+ {
+ return true;
+ }
+
+ // Crouchjumped or perfed longjumps/bhops can have a maximum of 101 airtime
+ // when the lj bug occurs. Since we've fixed that the airtime is valid.
+ if (this.jump.duration <= 101)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ void Callback()
+ {
+ if (GOKZ_JS_GetOption(this.jumper, JSOption_JumpstatsAlways) == JSToggleOption_Enabled)
+ {
+ Call_OnJumpstatAlways(this.jump);
+ }
+ else
+ {
+ Call_OnLanding(this.jump);
+ }
+ }
+
+
+
+ // =====[ ALWAYS FAILSTATS ]==================================================
+
+ void AlwaysFailstat()
+ {
+ bool foundBlock;
+ int coordDist, distSign;
+ float traceStart[3], traceEnd[3], tracePos[3], landingPos[3], orientation[3], failOrigin[3];
+
+ // Check whether the jump was already handled
+ if (this.jump.type == JumpType_FullInvalid || this.failstatValid)
+ {
+ return;
+ }
+
+ // Initialize the trace boxes
+ float traceMins[3] = { 0.0, 0.0, 0.0 };
+ float traceLongMaxs[3] = { 0.0, 0.0, 200.0 };
+ float traceShortMaxs[3] = { 0.0, 0.0, 54.0 };
+
+ // Clear the stats
+ this.jump.miss = 0.0;
+ this.jump.distance = 0.0;
+
+ // Calculate the edge
+ this.CalcAlwaysEdge();
+
+ // We will search for the block based on the direction the player was looking
+ CopyVector(pose(0).orientation, orientation);
+
+ // Get the landing orientation
+ coordDist = FloatAbs(orientation[0]) < FloatAbs(orientation[1]);
+ distSign = orientation[coordDist] > 0 ? 1 : -1;
+
+ // Initialize the traces
+ CopyVector(this.position, traceStart);
+ CopyVector(this.position, traceEnd);
+
+ // Assume the miss is less than 100 units
+ traceEnd[coordDist] += 100.0 * distSign;
+
+ // Search for the end block with the long trace
+ foundBlock = TraceHullPosition(traceStart, traceEnd, traceMins, traceLongMaxs, tracePos);
+
+ // If not even the long trace finds the block, we're out of luck
+ if (foundBlock)
+ {
+ // Search for the block height
+ tracePos[2] = this.position[2];
+ foundBlock = this.TryFindBlockHeight(tracePos, landingPos, coordDist, distSign);
+
+ // Maybe there was a headbanger, try with the short trace instead
+ if (!foundBlock)
+ {
+ if (TraceHullPosition(traceStart, traceEnd, traceMins, traceShortMaxs, tracePos))
+ {
+ // Search for the height again
+ tracePos[2] = this.position[2];
+ foundBlock = this.TryFindBlockHeight(tracePos, landingPos, coordDist, distSign);
+ }
+ }
+
+ if (foundBlock)
+ {
+ // Search for the last tick the player was above the landing block elevation.
+ for (int i = 0; i < JS_FAILSTATS_MAX_TRACKED_TICKS; i++)
+ {
+ Pose p;
+
+ // This copies it, but it shouldn't be that much of a problem
+ p = pose(-i);
+
+ if(p.position[2] >= landingPos[2])
+ {
+ // Calculate the correct fail position
+ this.GetFailOrigin(landingPos[2], failOrigin, -i);
+
+ // Calculate all missing stats
+ this.jump.miss = FloatAbs(failOrigin[coordDist] - landingPos[coordDist]) - 16.0;
+ this.jump.distance = GetVectorHorizontalDistance(failOrigin, this.takeoffOrigin);
+ this.jump.offset = failOrigin[2] - this.takeoffOrigin[2];
+ this.jump.duration = p.duration;
+ this.jump.overlap = p.overlap;
+ this.jump.deadair = p.deadair;
+ this.jump.sync = float(p.syncTicks) / float(this.jump.duration) * 100.0;
+ break;
+ }
+ }
+ }
+ }
+
+ // Notify everyone about the jump
+ Call_OnFailstatAlways(this.jump);
+
+ // Fully invalidate the jump cause we failstatted it already
+ this.jump.type = JumpType_FullInvalid;
+ }
+
+ void CalcAlwaysEdge()
+ {
+ int coordDist, distSign;
+ float traceStart[3], traceEnd[3], velocity[3];
+ float ladderNormal[3], ladderMins[3], ladderMaxs[3];
+
+ // Ladder jumps have a different definition of edge
+ if (this.jump.originalType == JumpType_LadderJump)
+ {
+ // Get a vector that points outwards from the lader towards the player
+ GetEntPropVector(this.jumper, Prop_Send, "m_vecLadderNormal", ladderNormal);
+
+ // Initialize box to search for the ladder
+ if (ladderNormal[0] > ladderNormal[1])
+ {
+ ladderMins = view_as<float>({ 0.0, -20.0, 0.0 });
+ ladderMaxs = view_as<float>({ 0.0, 20.0, 0.0 });
+ coordDist = 0;
+ }
+ else
+ {
+ ladderMins = view_as<float>({ -20.0, 0.0, 0.0 });
+ ladderMaxs = view_as<float>({ 20.0, 0.0, 0.0 });
+ coordDist = 1;
+ }
+
+ // The max the ladder will be away is the player model (16) + danvari tech (10) + a safety unit
+ CopyVector(this.takeoffOrigin, traceEnd);
+ traceEnd[coordDist] += 27.0;
+
+ // Search for the ladder
+ if (TraceHullPosition(this.takeoffOrigin, traceEnd, ladderMins, ladderMaxs, traceEnd))
+ {
+ this.jump.edge = FloatAbs(traceEnd[coordDist] - this.takeoffOrigin[coordDist]) - 16.0;
+ }
+ }
+ else
+ {
+ // We calculate the orientation of the takeoff block based on what
+ // direction the player was moving
+ CopyVector(this.takeoffVelocity, velocity);
+ this.jump.edge = -1.0;
+
+ // Calculate the takeoff orientation
+ coordDist = FloatAbs(velocity[0]) < FloatAbs(velocity[1]);
+ distSign = velocity[coordDist] > 0 ? 1 : -1;
+
+ // Make sure we hit the jumpoff block
+ CopyVector(this.takeoffOrigin, traceEnd);
+ traceEnd[coordDist] -= 16.0 * distSign;
+ traceEnd[2] -= 1.0;
+
+ // Assume a max edge of 20
+ CopyVector(traceEnd, traceStart);
+ traceStart[coordDist] += 20.0 * distSign;
+
+ // Trace the takeoff block
+ if (TraceRayPosition(traceStart, traceEnd, traceEnd))
+ {
+ // Check whether the trace was stuck in the block from the beginning
+ if (FloatAbs(traceEnd[coordDist] - traceStart[coordDist]) > EPSILON)
+ {
+ // Block trace ends 0.03125 in front of the actual block. Adjust the edge correctly.
+ this.jump.edge = FloatAbs(traceEnd[coordDist] - this.takeoffOrigin[coordDist] + (16.0 - 0.03125) * distSign);
+ }
+ }
+ }
+ }
+
+ bool TryFindBlockHeight(const float position[3], float result[3], int coordDist, int distSign)
+ {
+ float traceStart[3], traceEnd[3];
+
+ // Setup the trace points
+ CopyVector(position, traceStart);
+ traceStart[coordDist] += distSign;
+ CopyVector(traceStart, traceEnd);
+
+ // We search in 54 unit steps
+ traceStart[2] += 54.0;
+
+ // We search with multiple trace starts in case the landing block has a roof
+ for (int i = 0; i < 3; i += 1)
+ {
+ if (TraceRayPosition(traceStart, traceEnd, result))
+ {
+ // Make sure the trace didn't get stuck right away
+ if (FloatAbs(result[2] - traceStart[2]) > EPSILON)
+ {
+ result[coordDist] -= distSign;
+ return true;
+ }
+ }
+
+ // Try the next are to find the block. We use two different values to have
+ // some overlap in case the block perfectly aligns with the trace.
+ traceStart[2] += 54.0;
+ traceEnd[2] += 53.0;
+ }
+
+ return false;
+ }
+
+
+
+ // =====[ BLOCK STATS HELPERS ]===============================================
+
+ void CalcBlockStats(float landingOrigin[3], bool checkOffset = false)
+ {
+ int coordDist, coordDev, distSign;
+ float middle[3], startBlock[3], endBlock[3], sweepBoxMin[3], sweepBoxMax[3];
+
+ // Get the orientation of the block.
+ GetCoordOrientation(landingOrigin, this.takeoffOrigin, coordDist, distSign);
+ coordDev = !coordDist;
+
+ // We can't make measurements from within an entity, so we assume the
+ // player had a remotely reasonable edge and that the middle of the jump
+ // is not over a block and then start measuring things out from there.
+ middle[coordDist] = (this.takeoffOrigin[coordDist] + landingOrigin[coordDist]) / 2;
+ middle[coordDev] = (this.takeoffOrigin[coordDev] + landingOrigin[coordDev]) / 2;
+ middle[2] = this.takeoffOrigin[2] - 1.0;
+
+ // Get the deviation.
+ this.jump.deviation = FloatAbs(landingOrigin[coordDev] - this.takeoffOrigin[coordDev]);
+
+ // Setup a sweeping line that starts in the middle and tries to search for the smallest
+ // block within the deviation of the player.
+ sweepBoxMin[coordDist] = 0.0;
+ sweepBoxMin[coordDev] = -this.jump.deviation - 16.0;
+ sweepBoxMin[2] = 0.0;
+ sweepBoxMax[coordDist] = 0.0;
+ sweepBoxMax[coordDev] = this.jump.deviation + 16.0;
+ sweepBoxMax[2] = 0.0;
+
+ // Modify the takeoff and landing origins to line up with the middle and respect
+ // the bounding box of the player.
+ startBlock[coordDist] = this.takeoffOrigin[coordDist] - distSign * 16.0;
+ // Sometimes you can land 0.03125 units in front of a block, so the trace needs to be extended.
+ endBlock[coordDist] = landingOrigin[coordDist] + distSign * (16.0 + 0.03125);
+ startBlock[coordDev] = middle[coordDev];
+ endBlock[coordDev] = middle[coordDev];
+ startBlock[2] = middle[2];
+ endBlock[2] = middle[2];
+
+ // Search for the blocks
+ if (!TraceHullPosition(middle, startBlock, sweepBoxMin, sweepBoxMax, startBlock)
+ || !TraceHullPosition(middle, endBlock, sweepBoxMin, sweepBoxMax, endBlock))
+ {
+ return;
+ }
+
+ // Make sure the edges of the blocks are parallel.
+ if (!this.BlockAreEdgesParallel(startBlock, endBlock, this.jump.deviation + 32.0, coordDist, coordDev))
+ {
+ this.jump.block = 0;
+ this.jump.edge = -1.0;
+ return;
+ }
+
+ // Needed for failstats, but you need the endBlock position for that, so we do it here.
+ if (checkOffset)
+ {
+ endBlock[2] += 1.0;
+ if (FloatAbs(this.FindBlockHeight(endBlock, float(distSign) * 17.0, coordDist, 1.0) - landingOrigin[2]) > JS_OFFSET_EPSILON)
+ {
+ return;
+ }
+ }
+
+ // Calculate distance and edge.
+ this.jump.block = RoundFloat(FloatAbs(endBlock[coordDist] - startBlock[coordDist]));
+ // Block trace ends 0.03125 in front of the actual block. Adjust the edge correctly.
+ this.jump.edge = FloatAbs(startBlock[coordDist] - this.takeoffOrigin[coordDist] + (16.0 - 0.03125) * distSign);
+
+ // Make it easier to check for blocks that too short
+ if (this.jump.block < JS_MIN_BLOCK_DISTANCE)
+ {
+ this.jump.block = 0;
+ this.jump.edge = -1.0;
+ }
+ }
+
+ void CalcLadderBlockStats(float landingOrigin[3], bool checkOffset = false)
+ {
+ int coordDist, coordDev, distSign;
+ float sweepBoxMin[3], sweepBoxMax[3], blockPosition[3], ladderPosition[3], normalVector[3], endBlock[3], middle[3];
+
+ // Get the orientation of the block.
+ GetCoordOrientation(landingOrigin, this.takeoffOrigin, coordDist, distSign);
+ coordDev = !coordDist;
+
+ // Get the deviation.
+ this.jump.deviation = FloatAbs(landingOrigin[coordDev] - this.takeoffOrigin[coordDev]);
+
+ // Make sure the ladder is aligned.
+ GetEntPropVector(this.jumper, Prop_Send, "m_vecLadderNormal", normalVector);
+ if (FloatAbs(FloatAbs(normalVector[coordDist]) - 1.0) > EPSILON)
+ {
+ return;
+ }
+
+ // Make sure we'll find the block and ladder.
+ CopyVector(this.takeoffOrigin, ladderPosition);
+ CopyVector(landingOrigin, endBlock);
+ endBlock[2] -= 1.0;
+ ladderPosition[2] = endBlock[2];
+
+ // Setup a line to search for the ladder.
+ sweepBoxMin[coordDist] = 0.0;
+ sweepBoxMin[coordDev] = -20.0;
+ sweepBoxMin[2] = 0.0;
+ sweepBoxMax[coordDist] = 0.0;
+ sweepBoxMax[coordDev] = 20.0;
+ sweepBoxMax[2] = 0.0;
+ middle[coordDist] = ladderPosition[coordDist] + distSign * JS_MIN_LAJ_BLOCK_DISTANCE;
+ middle[coordDev] = endBlock[coordDev];
+ middle[2] = ladderPosition[2];
+
+ // Search for the ladder.
+ if (!TraceHullPosition(ladderPosition, middle, sweepBoxMin, sweepBoxMax, ladderPosition))
+ {
+ return;
+ }
+
+ // Find the block and make sure it's aligned
+ endBlock[coordDist] += distSign * 16.0;
+ if (!TraceRayPositionNormal(middle, endBlock, blockPosition, normalVector)
+ || FloatAbs(FloatAbs(normalVector[coordDist]) - 1.0) > EPSILON)
+ {
+ return;
+ }
+
+ // Needed for failstats, but you need the blockPosition for that, so we do it here.
+ if (checkOffset)
+ {
+ blockPosition[2] += 1.0;
+ if (!this.TraceLadderOffset(this.FindBlockHeight(blockPosition, float(distSign), coordDist, 1.0) - 0.031250))
+ {
+ return;
+ }
+ }
+
+ // Calculate distance and edge.
+ this.jump.block = RoundFloat(FloatAbs(blockPosition[coordDist] - ladderPosition[coordDist]));
+ this.jump.edge = FloatAbs(this.takeoffOrigin[coordDist] - ladderPosition[coordDist]) - 16.0;
+
+ // Make it easier to check for blocks that too short
+ if (this.jump.block < JS_MIN_LAJ_BLOCK_DISTANCE)
+ {
+ this.jump.block = 0;
+ this.jump.edge = -1.0;
+ }
+ }
+
+ bool TraceLadderOffset(float landingHeight)
+ {
+ float traceOrigin[3], traceEnd[3], ladderTop[3], ladderNormal[3];
+
+ // Get normal vector of the ladder.
+ GetEntPropVector(this.jumper, Prop_Send, "m_vecLadderNormal", ladderNormal);
+
+ // 10 units is the furthest away from the ladder surface you can get while still being on the ladder.
+ traceOrigin[0] = this.takeoffOrigin[0] - 10.0 * ladderNormal[0];
+ traceOrigin[1] = this.takeoffOrigin[1] - 10.0 * ladderNormal[1];
+ traceOrigin[2] = this.takeoffOrigin[2] + 5;
+
+ CopyVector(traceOrigin, traceEnd);
+ traceEnd[2] = this.takeoffOrigin[2] - 10;
+
+ // Search for the ladder
+ if (!TraceHullPosition(traceOrigin, traceEnd, playerMinsEx, playerMaxsEx, ladderTop)
+ || FloatAbs(ladderTop[2] - landingHeight) > JS_OFFSET_EPSILON)
+ {
+ this.Invalidate();
+ return false;
+ }
+ return true;
+ }
+
+ bool BlockTraceAligned(const float origin[3], const float end[3], int coordDist)
+ {
+ float normalVector[3];
+ if (!TraceRayNormal(origin, end, normalVector))
+ {
+ return false;
+ }
+ return FloatAbs(FloatAbs(normalVector[coordDist]) - 1.0) <= EPSILON;
+ }
+
+ bool BlockAreEdgesParallel(const float startBlock[3], const float endBlock[3], float deviation, int coordDist, int coordDev)
+ {
+ float start[3], end[3], offset;
+
+ // We use very short rays to find the blocks where they're supposed to be and use
+ // their normals to determine whether they're parallel or not.
+ offset = startBlock[coordDist] > endBlock[coordDist] ? 0.1 : -0.1;
+
+ // We search for the blocks on both sides of the player, on one of the sides
+ // there has to be a valid block.
+ start[coordDist] = startBlock[coordDist] - offset;
+ start[coordDev] = startBlock[coordDev] - deviation;
+ start[2] = startBlock[2];
+
+ end[coordDist] = startBlock[coordDist] + offset;
+ end[coordDev] = startBlock[coordDev] - deviation;
+ end[2] = startBlock[2];
+
+ if (this.BlockTraceAligned(start, end, coordDist))
+ {
+ start[coordDist] = endBlock[coordDist] + offset;
+ end[coordDist] = endBlock[coordDist] - offset;
+ if (this.BlockTraceAligned(start, end, coordDist))
+ {
+ return true;
+ }
+ start[coordDist] = startBlock[coordDist] - offset;
+ end[coordDist] = startBlock[coordDist] + offset;
+ }
+
+ start[coordDev] = startBlock[coordDev] + deviation;
+ end[coordDev] = startBlock[coordDev] + deviation;
+
+ if (this.BlockTraceAligned(start, end, coordDist))
+ {
+ start[coordDist] = endBlock[coordDist] + offset;
+ end[coordDist] = endBlock[coordDist] - offset;
+ if (this.BlockTraceAligned(start, end, coordDist))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ float FindBlockHeight(const float origin[3], float offset, int coord, float searchArea)
+ {
+ float block[3], traceStart[3], traceEnd[3], normalVector[3];
+
+ // Setup the trace.
+ CopyVector(origin, traceStart);
+ traceStart[coord] += offset;
+ CopyVector(traceStart, traceEnd);
+ traceStart[2] += searchArea;
+ traceEnd[2] -= searchArea;
+
+ // Find the block height.
+ if (!TraceRayPositionNormal(traceStart, traceEnd, block, normalVector)
+ || FloatAbs(normalVector[2] - 1.0) > EPSILON)
+ {
+ return -99999999999999999999.0; // Let's hope that's wrong enough
+ }
+
+ return block[2];
+ }
+
+ void GetFailOrigin(float planeHeight, float result[3], int poseIndex)
+ {
+ float newVel[3], oldVel[3];
+
+ // Calculate the actual velocity.
+ CopyVector(pose(poseIndex).velocity, oldVel);
+ ScaleVector(oldVel, GetTickInterval());
+
+ // Calculate at which percentage of the velocity vector we hit the plane.
+ float scale = (planeHeight - pose(poseIndex).position[2]) / oldVel[2];
+
+ // Calculate the position we hit the plane.
+ CopyVector(oldVel, newVel);
+ ScaleVector(newVel, scale);
+ AddVectors(pose(poseIndex).position, newVel, result);
+ }
+}
+
+static JumpTracker jumpTrackers[MAXPLAYERS + 1];
+
+
+
+// =====[ HELPER FUNCTIONS ]===================================================
+
+void GetCoordOrientation(const float vec1[3], const float vec2[3], int &coordDist, int &distSign)
+{
+ coordDist = FloatAbs(vec1[0] - vec2[0]) < FloatAbs(vec1[1] - vec2[1]);
+ distSign = vec1[coordDist] > vec2[coordDist] ? 1 : -1;
+}
+
+bool TraceRayPosition(const float traceStart[3], const float traceEnd[3], float position[3])
+{
+ Handle trace = TR_TraceRayFilterEx(traceStart, traceEnd, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ TR_GetEndPosition(position, trace);
+ delete trace;
+ return true;
+ }
+ delete trace;
+ return false;
+}
+
+static bool TraceRayNormal(const float traceStart[3], const float traceEnd[3], float rayNormal[3])
+{
+ Handle trace = TR_TraceRayFilterEx(traceStart, traceEnd, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ TR_GetPlaneNormal(trace, rayNormal);
+ delete trace;
+ return true;
+ }
+ delete trace;
+ return false;
+}
+
+static bool TraceRayPositionNormal(const float traceStart[3], const float traceEnd[3], float position[3], float rayNormal[3])
+{
+ Handle trace = TR_TraceRayFilterEx(traceStart, traceEnd, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ TR_GetEndPosition(position, trace);
+ TR_GetPlaneNormal(trace, rayNormal);
+ delete trace;
+ return true;
+ }
+ delete trace;
+ return false;
+}
+
+static bool TraceHullPosition(const float traceStart[3], const float traceEnd[3], const float mins[3], const float maxs[3], float position[3])
+{
+ Handle trace = TR_TraceHullFilterEx(traceStart, traceEnd, mins, maxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ TR_GetEndPosition(position, trace);
+ delete trace;
+ return true;
+ }
+ delete trace;
+ return false;
+}
+
+
+
+// =====[ EVENTS ]=============================================================
+
+void OnPluginStart_JumpTracking()
+{
+ GameData gd = LoadGameConfigFile("sdktools.games/engine.csgo");
+ int offset = gd.GetOffset("AcceptInput");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get AcceptInput offset");
+ }
+
+ acceptInputHook = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, DHooks_AcceptInput);
+ DHookAddParam(acceptInputHook, HookParamType_CharPtr);
+ DHookAddParam(acceptInputHook, HookParamType_CBaseEntity);
+ DHookAddParam(acceptInputHook, HookParamType_CBaseEntity);
+ //varaint_t is a union of 12 (float[3]) plus two int type params 12 + 8 = 20
+ DHookAddParam(acceptInputHook, HookParamType_Object, 20, DHookPass_ByVal|DHookPass_ODTOR|DHookPass_OCTOR|DHookPass_OASSIGNOP);
+ DHookAddParam(acceptInputHook, HookParamType_Int);
+ delete gd;
+}
+
+void OnOptionChanged_JumpTracking(int client, const char[] option)
+{
+ if (StrEqual(option, gC_CoreOptionNames[Option_Mode]))
+ {
+ jumpTrackers[client].jump.type = JumpType_FullInvalid;
+ }
+}
+
+void OnClientPutInServer_JumpTracking(int client)
+{
+ if (entityTouchList[client] != INVALID_HANDLE)
+ {
+ delete entityTouchList[client];
+ }
+ entityTouchList[client] = new ArrayList();
+ lastNoclipTime[client] = 0;
+ lastDuckbugTime[client] = 0;
+ lastJumpButtonTime[client] = 0.0;
+ jumpTrackers[client].Init(client);
+ DHookEntity(acceptInputHook, true, client);
+}
+
+
+// This was originally meant for invalidating jumpstats but was removed.
+void OnJumpInvalidated_JumpTracking(int client)
+{
+ jumpTrackers[client].Invalidate();
+}
+
+void OnJumpValidated_JumpTracking(int client, bool jumped, bool ladderJump, bool jumpbug)
+{
+ if (!validCmd[client])
+ {
+ return;
+ }
+
+ // Update: Takeoff speed should be always correct with the new MovementAPI.
+ if (jumped)
+ {
+ jumpTrackers[client].lastJumpTick = jumpTrackers[client].tickCount;
+ }
+ jumpTrackers[client].Reset(jumped, ladderJump, jumpbug);
+ jumpTrackers[client].Begin();
+}
+
+void OnStartTouchGround_JumpTracking(int client)
+{
+ if (!doFailstatAlways[client])
+ {
+ jumpTrackers[client].End();
+ }
+}
+
+void OnStartTouch_JumpTracking(int client, int touched)
+{
+ if (entityTouchList[client] != INVALID_HANDLE)
+ {
+ entityTouchList[client].Push(touched);
+ // Do not immediately invalidate jumps upon collision.
+ // Give the player a few ticks of leniency for late ducking.
+ }
+}
+
+void OnTouch_JumpTracking(int client)
+{
+ if (entityTouchList[client] != INVALID_HANDLE && entityTouchList[client].Length > 0)
+ {
+ entityTouchDuration[client]++;
+ }
+ if (!Movement_GetOnGround(client) && entityTouchDuration[client] > JS_TOUCH_GRACE_TICKS)
+ {
+ jumpTrackers[client].Invalidate();
+ }
+}
+
+void OnEndTouch_JumpTracking(int client, int touched)
+{
+ if (entityTouchList[client] != INVALID_HANDLE)
+ {
+ int index = entityTouchList[client].FindValue(touched);
+ if (index != -1)
+ {
+ entityTouchList[client].Erase(index);
+ }
+ if (entityTouchList[client].Length == 0)
+ {
+ entityTouchDuration[client] = 0;
+ }
+ }
+}
+
+void OnPlayerRunCmd_JumpTracking(int client, int buttons, int tickcount)
+{
+ if (!IsValidClient(client) || !IsPlayerAlive(client))
+ {
+ return;
+ }
+
+ jumpTrackers[client].tickCount = tickcount;
+
+ if (GetClientButtons(client) & IN_JUMP)
+ {
+ lastJumpButtonTime[client] = GetGameTime();
+ }
+
+ if (CheckNoclip(client))
+ {
+ lastNoclipTime[client] = tickcount;
+ }
+
+ // Don't bother checking if player is already in air and jumpstat is already invalid
+ if (Movement_GetOnGround(client) ||
+ jumpTrackers[client].jump.type != JumpType_FullInvalid)
+ {
+ UpdateValidCmd(client, buttons);
+ }
+}
+
+public Action Movement_OnWalkMovePost(int client)
+{
+ lastGroundSpeedCappedTime[client] = jumpTrackers[client].tickCount;
+ return Plugin_Continue;
+}
+
+public Action Movement_OnPlayerMovePost(int client)
+{
+ lastMovementProcessedTime[client] = jumpTrackers[client].tickCount;
+ return Plugin_Continue;
+}
+
+public void OnPlayerRunCmdPost_JumpTracking(int client)
+{
+ if (!IsValidClient(client) || !IsPlayerAlive(client))
+ {
+ return;
+ }
+
+ // Check for always failstats
+ if (doFailstatAlways[client])
+ {
+ doFailstatAlways[client] = false;
+ // Prevent TP shenanigans that would trigger failstats
+ //jumpTypeLast[client] = JumpType_Invalid;
+
+ if (GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Enabled &&
+ isInAir[client])
+ {
+ jumpTrackers[client].AlwaysFailstat();
+ }
+ }
+
+ if (!Movement_GetOnGround(client))
+ {
+ isInAir[client] = true;
+ jumpTrackers[client].Update();
+ }
+
+ if (Movement_GetOnGround(client) ||
+ Movement_GetMovetype(client) == MOVETYPE_LADDER)
+ {
+ isInAir[client] = false;
+ jumpTrackers[client].UpdateOnGround();
+ }
+
+ // We always have to track this, no matter if in the air or not
+ jumpTrackers[client].UpdateRelease();
+
+ if (Movement_GetDuckbugged(client))
+ {
+ lastDuckbugTime[client] = jumpTrackers[client].tickCount;
+ }
+}
+
+static MRESReturn DHooks_AcceptInput(int client, DHookReturn hReturn, DHookParam hParams)
+{
+ if (!IsValidClient(client) || !IsPlayerAlive(client))
+ {
+ return MRES_Ignored;
+ }
+
+ // Get args
+ static char param[64];
+ static char command[64];
+ DHookGetParamString(hParams, 1, command, sizeof(command));
+ if (StrEqual(command, "AddOutput"))
+ {
+ DHookGetParamObjectPtrString(hParams, 4, 0, ObjectValueType_String, param, sizeof(param));
+ char kv[16];
+ SplitString(param, " ", kv, sizeof(kv));
+ // KVs are case insensitive.
+ if (StrEqual(kv[0], "origin", false))
+ {
+ // The player technically did not get "teleported" but the origin gets changed regardless,
+ // which effectively is a teleport.
+ OnTeleport_FailstatAlways(client);
+ }
+ }
+ return MRES_Ignored;
+}
+
+// =====[ CHECKS ]=====
+
+static void UpdateValidCmd(int client, int buttons)
+{
+ if (!CheckGravity(client)
+ || !CheckBaseVelocity(client)
+ || !CheckInWater(client)
+ || !CheckTurnButtons(buttons))
+ {
+ InvalidateJumpstat(client);
+ validCmd[client] = false;
+ }
+ else
+ {
+ validCmd[client] = true;
+ }
+
+ if (jumpTrackers[client].tickCount - lastNoclipTime[client] < GOKZ_JUMPSTATS_NOCLIP_RESET_TICKS)
+ {
+ jumpTrackers[client].jump.type = JumpType_FullInvalid;
+ }
+
+ if (!CheckLadder(client))
+ {
+ InvalidateJumpstat(client);
+ }
+}
+
+static bool CheckGravity(int client)
+{
+ float gravity = Movement_GetGravity(client);
+ // Allow 1.0 and 0.0 gravity as both values appear during normal gameplay
+ if (FloatAbs(gravity - 1.0) > EPSILON && FloatAbs(gravity) > EPSILON)
+ {
+ return false;
+ }
+ return true;
+}
+
+static bool CheckBaseVelocity(int client)
+{
+ float baseVelocity[3];
+ Movement_GetBaseVelocity(client, baseVelocity);
+ if (FloatAbs(baseVelocity[0]) > EPSILON ||
+ FloatAbs(baseVelocity[1]) > EPSILON ||
+ FloatAbs(baseVelocity[2]) > EPSILON)
+ {
+ return false;
+ }
+ return true;
+}
+
+static bool CheckInWater(int client)
+{
+ int waterLevel = GetEntProp(client, Prop_Data, "m_nWaterLevel");
+ return waterLevel == 0;
+}
+
+static bool CheckTurnButtons(int buttons)
+{
+ // Don't allow +left or +right turns binds
+ return !(buttons & (IN_LEFT | IN_RIGHT));
+}
+
+static bool CheckNoclip(int client)
+{
+ return Movement_GetMovetype(client) == MOVETYPE_NOCLIP;
+}
+
+static bool CheckLadder(int client)
+{
+ return Movement_GetMovetype(client) != MOVETYPE_LADDER;
+}
+
+
+
+// =====[ EXTERNAL HELPER FUNCTIONS ]==========================================
+
+void InvalidateJumpstat(int client)
+{
+ jumpTrackers[client].Invalidate();
+}
+
+float GetStrafeSync(Jump jump, int strafe)
+{
+ if (strafe < JS_MAX_TRACKED_STRAFES)
+ {
+ return float(jump.strafes_gainTicks[strafe])
+ / float(jump.strafes_ticks[strafe])
+ * 100.0;
+ }
+ else
+ {
+ return 0.0;
+ }
+}
+
+float GetStrafeAirtime(Jump jump, int strafe)
+{
+ if (strafe < JS_MAX_TRACKED_STRAFES)
+ {
+ return float(jump.strafes_ticks[strafe])
+ / float(jump.duration)
+ * 100.0;
+ }
+ else
+ {
+ return 0.0;
+ }
+}
+
+void OnTeleport_FailstatAlways(int client)
+{
+ // We want to synchronize all of that
+ doFailstatAlways[client] = true;
+
+ // gokz-core does that too, but for some reason we have to do it again
+ InvalidateJumpstat(client);
+
+ jumpTrackers[client].lastTeleportTick = jumpTrackers[client].tickCount;
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/jump_validating.sp b/sourcemod/scripting/gokz-jumpstats/jump_validating.sp
new file mode 100644
index 0000000..c6835c7
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/jump_validating.sp
@@ -0,0 +1,82 @@
+/*
+ Invalidating invalid jumps, such as ones with a modified velocity.
+*/
+
+static Handle processMovementHookPost;
+
+void OnPluginStart_JumpValidating()
+{
+ Handle gamedataConf = LoadGameConfigFile("gokz-core.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-core gamedata");
+ }
+
+ // CreateInterface
+ // Thanks SlidyBat and ici
+ StartPrepSDKCall(SDKCall_Static);
+ if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Signature, "CreateInterface"))
+ {
+ SetFailState("Failed to get CreateInterface");
+ }
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+ Handle CreateInterface = EndPrepSDKCall();
+
+ if (CreateInterface == null)
+ {
+ SetFailState("Unable to prepare SDKCall for CreateInterface");
+ }
+
+ char interfaceName[64];
+
+ // ProcessMovement
+ if (!GameConfGetKeyValue(gamedataConf, "IGameMovement", interfaceName, sizeof(interfaceName)))
+ {
+ SetFailState("Failed to get IGameMovement interface name");
+ }
+ Address IGameMovement = SDKCall(CreateInterface, interfaceName, 0);
+ if (!IGameMovement)
+ {
+ SetFailState("Failed to get IGameMovement pointer");
+ }
+
+ int offset = GameConfGetOffset(gamedataConf, "ProcessMovement");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get ProcessMovement offset");
+ }
+
+ processMovementHookPost = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPost);
+ DHookAddParam(processMovementHookPost, HookParamType_CBaseEntity);
+ DHookAddParam(processMovementHookPost, HookParamType_ObjectPtr);
+ DHookRaw(processMovementHookPost, false, IGameMovement);
+}
+
+static MRESReturn DHook_ProcessMovementPost(Handle hParams)
+{
+ int client = DHookGetParam(hParams, 1);
+ if (!IsValidClient(client) || IsFakeClient(client))
+ {
+ return MRES_Ignored;
+ }
+ float pVelocity[3], velocity[3];
+ Movement_GetProcessingVelocity(client, pVelocity);
+ Movement_GetVelocity(client, velocity);
+
+ gB_SpeedJustModifiedExternally[client] = false;
+ for (int i = 0; i < 3; i++)
+ {
+ if (FloatAbs(pVelocity[i] - velocity[i]) > EPSILON)
+ {
+ // The current velocity doesn't match the velocity of the end of movement processing,
+ // so it must have been modified by something like a trigger.
+ InvalidateJumpstat(client);
+ gB_SpeedJustModifiedExternally[client] = true;
+ break;
+ }
+ }
+
+ return MRES_Ignored;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-jumpstats/options.sp b/sourcemod/scripting/gokz-jumpstats/options.sp
new file mode 100644
index 0000000..7e0e9e9
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/options.sp
@@ -0,0 +1,86 @@
+/*
+ Options for jumpstats, including an option to disable it completely.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetJumpstatsDisabled(int client)
+{
+ return GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled
+ || (GOKZ_JS_GetOption(client, JSOption_MinChatTier) == DistanceTier_None
+ && GOKZ_JS_GetOption(client, JSOption_MinConsoleTier) == DistanceTier_None
+ && GOKZ_JS_GetOption(client, JSOption_MinSoundTier) == DistanceTier_None
+ && GOKZ_JS_GetOption(client, JSOption_FailstatsConsole) == JSToggleOption_Disabled
+ && GOKZ_JS_GetOption(client, JSOption_FailstatsChat) == JSToggleOption_Disabled
+ && GOKZ_JS_GetOption(client, JSOption_JumpstatsAlways) == JSToggleOption_Disabled);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void OnClientPutInServer_Options(int client)
+{
+ if (GOKZ_JS_GetOption(client, JSOption_MinSoundTier) == DistanceTier_Meh)
+ {
+ GOKZ_JS_SetOption(client, JSOption_MinSoundTier, DistanceTier_Impressive);
+ }
+}
+
+void OnOptionChanged_Options(int client, const char[] option, any newValue)
+{
+ JSOption jsOption;
+ if (GOKZ_JS_IsJSOption(option, jsOption))
+ {
+ if (jsOption == JSOption_MinSoundTier && newValue == DistanceTier_Meh)
+ {
+ GOKZ_JS_SetOption(client, JSOption_MinSoundTier, DistanceTier_Impressive);
+ }
+ else
+ {
+ PrintOptionChangeMessage(client, jsOption, newValue);
+ }
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void RegisterOptions()
+{
+ for (JSOption option; option < JSOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_JSOptionNames[option], gC_JSOptionDescriptions[option],
+ OptionType_Int, gI_JSOptionDefaults[option], 0, gI_JSOptionCounts[option] - 1);
+ }
+}
+
+static void PrintOptionChangeMessage(int client, JSOption option, any newValue)
+{
+ // NOTE: Not all options have a message for when they are changed.
+ switch (option)
+ {
+ case JSOption_JumpstatsMaster:
+ {
+ switch (newValue)
+ {
+ case JSToggleOption_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Master Switch - Enable");
+ }
+ case JSToggleOption_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Jumpstats Option - Master Switch - Disable");
+ }
+ }
+ }
+ }
+}
diff --git a/sourcemod/scripting/gokz-jumpstats/options_menu.sp b/sourcemod/scripting/gokz-jumpstats/options_menu.sp
new file mode 100644
index 0000000..903a8bb
--- /dev/null
+++ b/sourcemod/scripting/gokz-jumpstats/options_menu.sp
@@ -0,0 +1,145 @@
+static TopMenu optionsTopMenu;
+static TopMenuObject catJumpstats;
+static TopMenuObject itemsJumpstats[JSOPTION_COUNT];
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayJumpstatsOptionsMenu(int client)
+{
+ optionsTopMenu.DisplayCategory(catJumpstats, client);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu)
+{
+ if (optionsTopMenu == topMenu && catJumpstats != INVALID_TOPMENUOBJECT)
+ {
+ return;
+ }
+
+ catJumpstats = topMenu.AddCategory(JS_OPTION_CATEGORY, TopMenuHandler_Categories);
+}
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ // Make sure category exists
+ if (catJumpstats == INVALID_TOPMENUOBJECT)
+ {
+ GOKZ_OnOptionsMenuCreated(topMenu);
+ }
+
+ if (optionsTopMenu == topMenu)
+ {
+ return;
+ }
+
+ optionsTopMenu = topMenu;
+
+ // Add HUD option items
+ for (int option = 0; option < view_as<int>(JSOPTION_COUNT); option++)
+ {
+ itemsJumpstats[option] = optionsTopMenu.AddItem(gC_JSOptionNames[option], TopMenuHandler_HUD, catJumpstats);
+ }
+}
+
+public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == catJumpstats)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - Jumpstats", param);
+ }
+ }
+}
+
+public void TopMenuHandler_HUD(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ JSOption option = JSOPTION_INVALID;
+ for (int i = 0; i < view_as<int>(JSOPTION_COUNT); i++)
+ {
+ if (topobj_id == itemsJumpstats[i])
+ {
+ option = view_as<JSOption>(i);
+ break;
+ }
+ }
+
+ if (option == JSOPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ if (option == JSOption_JumpstatsMaster ||
+ option == JSOption_ExtendedChatReport ||
+ option == JSOption_FailstatsConsole ||
+ option == JSOption_FailstatsChat ||
+ option == JSOption_JumpstatsAlways)
+ {
+ FormatToggleableOptionDisplay(param, option, buffer, maxlength);
+ }
+ else
+ {
+ FormatDistanceTierOptionDisplay(param, option, buffer, maxlength);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_JS_CycleOption(param, option);
+ optionsTopMenu.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void FormatToggleableOptionDisplay(int client, JSOption option, char[] buffer, int maxlength)
+{
+ if (GOKZ_JS_GetOption(client, option) == JSToggleOption_Disabled)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gI_JSOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gI_JSOptionPhrases[option], client,
+ "Options Menu - Enabled", client);
+ }
+}
+
+static void FormatDistanceTierOptionDisplay(int client, JSOption option, char[] buffer, int maxlength)
+{
+ int optionValue = GOKZ_JS_GetOption(client, option);
+ if (optionValue == DistanceTier_None) // Disabled
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gI_JSOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ // Add a plus sign to anything below the highest tier
+ if (optionValue < DISTANCETIER_COUNT - 1)
+ {
+ FormatEx(buffer, maxlength, "%T - %s+",
+ gI_JSOptionPhrases[option], client,
+ gC_DistanceTiers[optionValue]);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %s",
+ gI_JSOptionPhrases[option], client,
+ gC_DistanceTiers[optionValue]);
+ }
+ }
+}
diff --git a/sourcemod/scripting/gokz-localdb.sp b/sourcemod/scripting/gokz-localdb.sp
new file mode 100644
index 0000000..d9b07c2
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb.sp
@@ -0,0 +1,188 @@
+#include <sourcemod>
+
+#include <geoip>
+
+#include <gokz/core>
+#include <gokz/localdb>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/jumpstats>
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Local DB",
+ author = "DanZay",
+ description = "Provides database for players, maps, courses and times",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-localdb.txt"
+
+Database gH_DB = null;
+DatabaseType g_DBType = DatabaseType_None;
+bool gB_ClientSetUp[MAXPLAYERS + 1];
+bool gB_ClientPostAdminChecked[MAXPLAYERS + 1];
+bool gB_Cheater[MAXPLAYERS + 1];
+int gI_PBJSCache[MAXPLAYERS + 1][MODE_COUNT][JUMPTYPE_COUNT][JUMPSTATDB_CACHE_COUNT];
+bool gB_MapSetUp;
+int gI_DBCurrentMapID;
+
+#include "gokz-localdb/api.sp"
+#include "gokz-localdb/commands.sp"
+#include "gokz-localdb/options.sp"
+
+#include "gokz-localdb/db/sql.sp"
+#include "gokz-localdb/db/helpers.sp"
+#include "gokz-localdb/db/cache_js.sp"
+#include "gokz-localdb/db/create_tables.sp"
+#include "gokz-localdb/db/save_js.sp"
+#include "gokz-localdb/db/save_time.sp"
+#include "gokz-localdb/db/set_cheater.sp"
+#include "gokz-localdb/db/setup_client.sp"
+#include "gokz-localdb/db/setup_database.sp"
+#include "gokz-localdb/db/setup_map.sp"
+#include "gokz-localdb/db/setup_map_courses.sp"
+#include "gokz-localdb/db/timer_setup.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-localdb");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-localdb.phrases");
+
+ CreateGlobalForwards();
+ RegisterCommands();
+ DB_SetupDatabase();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ char auth[32];
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientAuthorized(client) && GetClientAuthId(client, AuthId_Engine, auth, sizeof(auth)))
+ {
+ OnClientAuthorized(client, auth);
+ }
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnConfigsExecuted()
+{
+ DB_SetupMap();
+}
+
+public void GOKZ_DB_OnMapSetup(int mapID)
+{
+ DB_SetupMapCourses();
+
+ for (int client = 1; client < MAXPLAYERS + 1; client++)
+ {
+ if (IsValidClient(client) && !IsFakeClient(client) &&
+ gB_ClientPostAdminChecked[client] &&
+ GOKZ_GetOption(client, gC_DBOptionNames[DBOption_AutoLoadTimerSetup]) == DBOption_Enabled)
+ {
+ DB_LoadTimerSetup(client);
+ }
+ }
+}
+
+public void OnMapEnd()
+{
+ gB_MapSetUp = false;
+}
+
+public void OnClientAuthorized(int client, const char[] auth)
+{
+ DB_SetupClient(client);
+}
+
+public void OnClientPostAdminCheck(int client)
+{
+ // We need this after OnClientPutInServer cause that's where the VBs get reset
+ gB_ClientPostAdminChecked[client] = true;
+
+ if (gB_MapSetUp && GOKZ_GetOption(client, gC_DBOptionNames[DBOption_AutoLoadTimerSetup]) == DBOption_Enabled)
+ {
+ DB_LoadTimerSetup(client);
+ }
+}
+
+public void GOKZ_DB_OnClientSetup(int client, int steamID, bool cheater)
+{
+ DB_CacheJSPBs(client, steamID);
+}
+
+public void GOKZ_OnOptionsLoaded(int client)
+{
+ if (gB_MapSetUp && gB_ClientPostAdminChecked[client] && GOKZ_GetOption(client, gC_DBOptionNames[DBOption_AutoLoadTimerSetup]) == DBOption_Enabled)
+ {
+ DB_LoadTimerSetup(client);
+ }
+}
+
+public void OnClientDisconnect(int client)
+{
+ gB_ClientSetUp[client] = false;
+ gB_ClientPostAdminChecked[client] = false;
+}
+
+public void GOKZ_OnCourseRegistered(int course)
+{
+ if (gB_MapSetUp)
+ {
+ DB_SetupMapCourse(course);
+ }
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+ int style = GOKZ_GetCoreOption(client, Option_Style);
+ DB_SaveTime(client, course, mode, style, time, teleportsUsed);
+}
+
+public void GOKZ_JS_OnLanding(Jump jump)
+{
+ OnLanding_SaveJumpstat(jump);
+}
diff --git a/sourcemod/scripting/gokz-localdb/api.sp b/sourcemod/scripting/gokz-localdb/api.sp
new file mode 100644
index 0000000..a4bc29c
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/api.sp
@@ -0,0 +1,126 @@
+static GlobalForward H_OnDatabaseConnect;
+static GlobalForward H_OnClientSetup;
+static GlobalForward H_OnMapSetup;
+static GlobalForward H_OnTimeInserted;
+static GlobalForward H_OnJumpstatPB;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnDatabaseConnect = new GlobalForward("GOKZ_DB_OnDatabaseConnect", ET_Ignore, Param_Cell);
+ H_OnClientSetup = new GlobalForward("GOKZ_DB_OnClientSetup", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
+ H_OnMapSetup = new GlobalForward("GOKZ_DB_OnMapSetup", ET_Ignore, Param_Cell);
+ H_OnTimeInserted = new GlobalForward("GOKZ_DB_OnTimeInserted", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+ H_OnJumpstatPB = new GlobalForward("GOKZ_DB_OnJumpstatPB", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+}
+
+void Call_OnDatabaseConnect()
+{
+ Call_StartForward(H_OnDatabaseConnect);
+ Call_PushCell(g_DBType);
+ Call_Finish();
+}
+
+void Call_OnClientSetup(int client, int steamID, bool cheater)
+{
+ Call_StartForward(H_OnClientSetup);
+ Call_PushCell(client);
+ Call_PushCell(steamID);
+ Call_PushCell(cheater);
+ Call_Finish();
+}
+
+void Call_OnMapSetup()
+{
+ Call_StartForward(H_OnMapSetup);
+ Call_PushCell(gI_DBCurrentMapID);
+ Call_Finish();
+}
+
+void Call_OnTimeInserted(int client, int steamID, int mapID, int course, int mode, int style, int runTimeMS, int teleportsUsed)
+{
+ Call_StartForward(H_OnTimeInserted);
+ Call_PushCell(client);
+ Call_PushCell(steamID);
+ Call_PushCell(mapID);
+ Call_PushCell(course);
+ Call_PushCell(mode);
+ Call_PushCell(style);
+ Call_PushCell(runTimeMS);
+ Call_PushCell(teleportsUsed);
+ Call_Finish();
+}
+
+void Call_OnJumpstatPB(int client, int jumptype, int mode, float distance, int block, int strafes, float sync, float pre, float max, int airtime)
+{
+ Call_StartForward(H_OnJumpstatPB);
+ Call_PushCell(client);
+ Call_PushCell(jumptype);
+ Call_PushCell(mode);
+ Call_PushCell(distance);
+ Call_PushCell(block);
+ Call_PushCell(strafes);
+ Call_PushCell(sync);
+ Call_PushCell(pre);
+ Call_PushCell(max);
+ Call_PushCell(airtime);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_DB_GetDatabase", Native_GetDatabase);
+ CreateNative("GOKZ_DB_GetDatabaseType", Native_GetDatabaseType);
+ CreateNative("GOKZ_DB_IsClientSetUp", Native_IsClientSetUp);
+ CreateNative("GOKZ_DB_IsMapSetUp", Native_IsMapSetUp);
+ CreateNative("GOKZ_DB_GetCurrentMapID", Native_GetCurrentMapID);
+ CreateNative("GOKZ_DB_IsCheater", Native_IsCheater);
+ CreateNative("GOKZ_DB_SetCheater", Native_SetCheater);
+}
+
+public int Native_GetDatabase(Handle plugin, int numParams)
+{
+ if (gH_DB == null)
+ {
+ return view_as<int>(gH_DB);
+ }
+ return view_as<int>(CloneHandle(gH_DB));
+}
+
+public int Native_GetDatabaseType(Handle plugin, int numParams)
+{
+ return view_as<int>(g_DBType);
+}
+
+public int Native_IsClientSetUp(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_ClientSetUp[GetNativeCell(1)]);
+}
+
+public int Native_IsMapSetUp(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_MapSetUp);
+}
+
+public int Native_GetCurrentMapID(Handle plugin, int numParams)
+{
+ return gI_DBCurrentMapID;
+}
+
+public int Native_IsCheater(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_Cheater[GetNativeCell(1)]);
+}
+
+public int Native_SetCheater(Handle plugin, int numParams)
+{
+ DB_SetCheater(GetNativeCell(1), GetNativeCell(2));
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-localdb/commands.sp b/sourcemod/scripting/gokz-localdb/commands.sp
new file mode 100644
index 0000000..2410fc5
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/commands.sp
@@ -0,0 +1,199 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_savetimersetup", Command_SaveTimerSetup, "[KZ] Save the current timer setup (virtual buttons and start position).");
+ RegConsoleCmd("sm_sts", Command_SaveTimerSetup, "[KZ] Save the current timer setup (virtual buttons and start position).");
+ RegConsoleCmd("sm_loadtimersetup", Command_LoadTimerSetup, "[KZ] Load the saved timer setup (virtual buttons and start position).");
+ RegConsoleCmd("sm_lts", Command_LoadTimerSetup, "[KZ] Load the saved timer setup (virtual buttons and start position).");
+
+ RegAdminCmd("sm_setcheater", CommandSetCheater, ADMFLAG_ROOT, "[KZ] Set a SteamID as a cheater. Usage: !setcheater <STEAM_1:X:X>");
+ RegAdminCmd("sm_setnotcheater", CommandSetNotCheater, ADMFLAG_ROOT, "[KZ] Set a SteamID as not a cheater. Usage: !setnotcheater <STEAM_1:X:X>");
+ RegAdminCmd("sm_deletebestjump", CommandDeleteBestJump, ADMFLAG_ROOT, "[KZ] Remove the top jumpstat of a SteamID. Usage: !deletebestjump <STEAM_1:X:X> <mode> <jump type> <block?>");
+ RegAdminCmd("sm_deletealljumps", CommandDeleteAllJumps, ADMFLAG_ROOT, "[KZ] Remove all jumpstats of a SteamID. Usage: !deletealljumps <STEAM_1:X:X>");
+ RegAdminCmd("sm_deletejump", CommandDeleteJump, ADMFLAG_ROOT, "[KZ] Remove a jumpstat by it's id. Usage: !deletejump <id>");
+ RegAdminCmd("sm_deletetime", CommandDeleteTime, ADMFLAG_ROOT, "[KZ] Remove a time by it's id. Usage: !deletetime <id>");
+}
+
+public Action Command_SaveTimerSetup(int client, int args)
+{
+ DB_SaveTimerSetup(client);
+ return Plugin_Handled;
+}
+
+public Action Command_LoadTimerSetup(int client, int args)
+{
+ DB_LoadTimerSetup(client, true);
+ return Plugin_Handled;
+}
+
+public Action CommandSetCheater(int client, int args)
+{
+ if (args == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No SteamID specified");
+ return Plugin_Handled;
+ }
+
+ char steamID2[64];
+ GetCmdArgString(steamID2, sizeof(steamID2));
+ int steamAccountID = Steam2ToSteamAccountID(steamID2);
+ if (steamAccountID == -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID");
+ }
+ else
+ {
+ DB_SetCheaterSteamID(client, steamAccountID, true);
+ }
+
+ return Plugin_Handled;
+}
+
+public Action CommandSetNotCheater(int client, int args)
+{
+ if (args == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No SteamID specified");
+ }
+
+ char steamID2[64];
+ GetCmdArgString(steamID2, sizeof(steamID2));
+ int steamAccountID = Steam2ToSteamAccountID(steamID2);
+ if (steamAccountID == -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID");
+ }
+ else
+ {
+ DB_SetCheaterSteamID(client, steamAccountID, false);
+ }
+
+ return Plugin_Handled;
+}
+
+public Action CommandDeleteBestJump(int client, int args)
+{
+ if (args < 3)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Delete Best Jump Usage");
+ return Plugin_Handled;
+ }
+
+ int steamAccountID, isBlock, mode, jumpType;
+ char query[1024], split[4][32];
+
+ // Get arguments
+ split[3][0] = '\0';
+ GetCmdArgString(query, sizeof(query));
+ ExplodeString(query, " ", split, 4, 32, false);
+
+ // SteamID32
+ steamAccountID = Steam2ToSteamAccountID(split[0]);
+ if (steamAccountID == -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID");
+ return Plugin_Handled;
+ }
+
+ // Mode
+ for (mode = 0; mode < MODE_COUNT; mode++)
+ {
+ if (StrEqual(split[1], gC_ModeNames[mode]) || StrEqual(split[1], gC_ModeNamesShort[mode], false))
+ {
+ break;
+ }
+ }
+ if (mode == MODE_COUNT)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Mode");
+ return Plugin_Handled;
+ }
+
+ // Jumptype
+ for (jumpType = 0; jumpType < JUMPTYPE_COUNT; jumpType++)
+ {
+ if (StrEqual(split[2], gC_JumpTypes[jumpType]) || StrEqual(split[2], gC_JumpTypesShort[jumpType], false))
+ {
+ break;
+ }
+ }
+ if (jumpType == JUMPTYPE_COUNT)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Jumptype");
+ return Plugin_Handled;
+ }
+
+ // Is it a block jump?
+ isBlock = StrEqual(split[3], "yes", false) || StrEqual(split[3], "true", false) || StrEqual(split[3], "1");
+
+ DB_DeleteBestJump(client, steamAccountID, jumpType, mode, isBlock);
+
+ return Plugin_Handled;
+}
+
+public Action CommandDeleteAllJumps(int client, int args)
+{
+ if (args < 1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Delete All Jumps Usage");
+ return Plugin_Handled;
+ }
+
+ int steamAccountID;
+ char steamid[32];
+
+ GetCmdArgString(steamid, sizeof(steamid));
+ steamAccountID = Steam2ToSteamAccountID(steamid);
+ if (steamAccountID == -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid SteamID");
+ return Plugin_Handled;
+ }
+
+ DB_DeleteAllJumps(client, steamAccountID);
+
+ return Plugin_Handled;
+}
+
+public Action CommandDeleteJump(int client, int args)
+{
+ if (args < 1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Delete Jump Usage");
+ return Plugin_Handled;
+ }
+
+ char buffer[24];
+ int jumpID;
+ GetCmdArgString(buffer, sizeof(buffer));
+ if (StringToIntEx(buffer, jumpID) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Jump ID");
+ return Plugin_Handled;
+ }
+
+ DB_DeleteJump(client, jumpID);
+
+ return Plugin_Handled;
+}
+
+public Action CommandDeleteTime(int client, int args)
+{
+ if (args < 1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Delete Time Usage");
+ return Plugin_Handled;
+ }
+
+ char buffer[24];
+ int timeID;
+ GetCmdArgString(buffer, sizeof(buffer));
+ if (StringToIntEx(buffer, timeID) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Time ID");
+ return Plugin_Handled;
+ }
+
+ DB_DeleteTime(client, timeID);
+
+ return Plugin_Handled;
+}
diff --git a/sourcemod/scripting/gokz-localdb/db/cache_js.sp b/sourcemod/scripting/gokz-localdb/db/cache_js.sp
new file mode 100644
index 0000000..b0df708
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/cache_js.sp
@@ -0,0 +1,67 @@
+/*
+ Caches the player's personal best jumpstats.
+*/
+
+
+
+void DB_CacheJSPBs(int client, int steamID)
+{
+ ClearCache(client);
+
+ char query[1024];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ FormatEx(query, sizeof(query), sql_jumpstats_getpbs, steamID);
+ txn.AddQuery(query);
+
+ FormatEx(query, sizeof(query), sql_jumpstats_getblockpbs, steamID, steamID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_CacheJSPBs, DB_TxnFailure_Generic, GetClientUserId(client), DBPrio_High);
+}
+
+public void DB_TxnSuccess_CacheJSPBs(Handle db, int userID, int numQueries, Handle[] results, any[] queryData)
+{
+ int client = GetClientOfUserId(userID);
+ if (client < 1 || client > MaxClients || !IsClientAuthorized(client) || IsFakeClient(client))
+ {
+ return;
+ }
+
+ int distance, mode, jumpType, block;
+
+ while (SQL_FetchRow(results[0]))
+ {
+ distance = SQL_FetchInt(results[0], 0);
+ mode = SQL_FetchInt(results[0], 1);
+ jumpType = SQL_FetchInt(results[0], 2);
+
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Distance] = block;
+ }
+
+ while (SQL_FetchRow(results[1]))
+ {
+ distance = SQL_FetchInt(results[1], 0);
+ mode = SQL_FetchInt(results[1], 1);
+ jumpType = SQL_FetchInt(results[1], 2);
+ block = SQL_FetchInt(results[1], 3);
+
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_BlockDistance] = distance;
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Block] = block;
+ }
+}
+
+void ClearCache(int client)
+{
+ for (int mode = 0; mode < MODE_COUNT; mode += 1)
+ {
+ for (int type = 0; type < JUMPTYPE_COUNT; type += 1)
+ {
+ for (int cache = 0; cache < JUMPSTATDB_CACHE_COUNT; cache += 1)
+ {
+ gI_PBJSCache[client][mode][type][cache] = 0;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/create_tables.sp b/sourcemod/scripting/gokz-localdb/db/create_tables.sp
new file mode 100644
index 0000000..2138830
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/create_tables.sp
@@ -0,0 +1,36 @@
+/*
+ Table creation and alteration.
+*/
+
+
+
+void DB_CreateTables()
+{
+ Transaction txn = SQL_CreateTransaction();
+
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ txn.AddQuery(sqlite_players_create);
+ txn.AddQuery(sqlite_maps_create);
+ txn.AddQuery(sqlite_mapcourses_create);
+ txn.AddQuery(sqlite_times_create);
+ txn.AddQuery(sqlite_jumpstats_create);
+ txn.AddQuery(sqlite_vbpos_create);
+ txn.AddQuery(sqlite_startpos_create);
+ }
+ case DatabaseType_MySQL:
+ {
+ txn.AddQuery(mysql_players_create);
+ txn.AddQuery(mysql_maps_create);
+ txn.AddQuery(mysql_mapcourses_create);
+ txn.AddQuery(mysql_times_create);
+ txn.AddQuery(mysql_jumpstats_create);
+ txn.AddQuery(mysql_vbpos_create);
+ txn.AddQuery(mysql_startpos_create);
+ }
+ }
+
+ SQL_ExecuteTransaction(gH_DB, txn, _, DB_TxnFailure_Generic, _, DBPrio_High);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/helpers.sp b/sourcemod/scripting/gokz-localdb/db/helpers.sp
new file mode 100644
index 0000000..1eff866
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/helpers.sp
@@ -0,0 +1,18 @@
+/*
+ Database helper functions and callbacks.
+*/
+
+
+
+/* Error report callback for failed transactions */
+public void DB_TxnFailure_Generic(Handle db, any data, int numQueries, const char[] error, int failIndex, any[] queryData)
+{
+ LogError("Database transaction error: %s", error);
+}
+
+/* Error report callback for failed transactions which deletes the DataPack */
+public void DB_TxnFailure_Generic_DataPack(Handle db, DataPack data, int numQueries, const char[] error, int failIndex, any[] queryData)
+{
+ delete data;
+ LogError("Database transaction error: %s", error);
+}
diff --git a/sourcemod/scripting/gokz-localdb/db/save_js.sp b/sourcemod/scripting/gokz-localdb/db/save_js.sp
new file mode 100644
index 0000000..1d50754
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/save_js.sp
@@ -0,0 +1,291 @@
+/*
+ Inserts or updates the player's jumpstat into the database.
+*/
+
+
+
+public void OnLanding_SaveJumpstat(Jump jump)
+{
+ int mode = GOKZ_GetCoreOption(jump.jumper, Option_Mode);
+
+ // No tiers given for 'Invalid' jumps.
+ if (jump.type == JumpType_Invalid || jump.type == JumpType_FullInvalid
+ || jump.type == JumpType_Fall || jump.type == JumpType_Other
+ || jump.type != JumpType_LadderJump && jump.offset < -JS_OFFSET_EPSILON
+ || jump.distance > JS_MAX_JUMP_DISTANCE
+ || jump.type == JumpType_LadderJump && jump.distance < JS_MIN_LAJ_BLOCK_DISTANCE
+ || jump.type != JumpType_LadderJump && jump.distance < JS_MIN_BLOCK_DISTANCE)
+ {
+ return;
+ }
+
+ char query[1024];
+ DataPack data;
+ int steamid = GetSteamAccountID(jump.jumper);
+ int int_dist = RoundToNearest(jump.distance * GOKZ_DB_JS_DISTANCE_PRECISION);
+
+ // Non-block
+ if (gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Distance] == 0
+ || int_dist > gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Distance])
+ {
+ data = JSRecord_FillDataPack(jump, steamid, mode, false);
+ Transaction txn_noblock = SQL_CreateTransaction();
+ FormatEx(query, sizeof(query), sql_jumpstats_getrecord, steamid, jump.type, mode, 0);
+ txn_noblock.AddQuery(query);
+ SQL_ExecuteTransaction(gH_DB, txn_noblock, DB_TxnSuccess_LookupJSRecordForSave, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+ }
+
+ // Block
+ if (jump.block > 0
+ && (gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Block] == 0
+ || (jump.block > gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Block]
+ || jump.block == gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_Block]
+ && int_dist > gI_PBJSCache[jump.jumper][mode][jump.type][JumpstatDB_Cache_BlockDistance])))
+ {
+ data = JSRecord_FillDataPack(jump, steamid, mode, true);
+ Transaction txn_block = SQL_CreateTransaction();
+ FormatEx(query, sizeof(query), sql_jumpstats_getrecord, steamid, jump.type, mode, 1);
+ txn_block.AddQuery(query);
+ SQL_ExecuteTransaction(gH_DB, txn_block, DB_TxnSuccess_LookupJSRecordForSave, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+ }
+}
+
+static DataPack JSRecord_FillDataPack(Jump jump, int steamid, int mode, bool blockJump)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(jump.jumper);
+ data.WriteCell(steamid);
+ data.WriteCell(jump.type);
+ data.WriteCell(mode);
+ data.WriteCell(RoundToNearest(jump.distance * GOKZ_DB_JS_DISTANCE_PRECISION));
+ data.WriteCell(blockJump ? jump.block : 0);
+ data.WriteCell(jump.strafes);
+ data.WriteCell(RoundToNearest(jump.sync * GOKZ_DB_JS_SYNC_PRECISION));
+ data.WriteCell(RoundToNearest(jump.preSpeed * GOKZ_DB_JS_PRE_PRECISION));
+ data.WriteCell(RoundToNearest(jump.maxSpeed * GOKZ_DB_JS_MAX_PRECISION));
+ data.WriteCell(RoundToNearest(jump.duration * GetTickInterval() * GOKZ_DB_JS_AIRTIME_PRECISION));
+ return data;
+}
+
+public void DB_TxnSuccess_LookupJSRecordForSave(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = data.ReadCell();
+ int steamid = data.ReadCell();
+ int jumpType = data.ReadCell();
+ int mode = data.ReadCell();
+ int distance = data.ReadCell();
+ int block = data.ReadCell();
+ int strafes = data.ReadCell();
+ int sync = data.ReadCell();
+ int pre = data.ReadCell();
+ int max = data.ReadCell();
+ int airtime = data.ReadCell();
+
+ if (!IsValidClient(client))
+ {
+ delete data;
+ return;
+ }
+
+ char query[1024];
+ int rows = SQL_GetRowCount(results[0]);
+ if (rows == 0)
+ {
+ FormatEx(query, sizeof(query), sql_jumpstats_insert, steamid, jumpType, mode, distance, block > 0, block, strafes, sync, pre, max, airtime);
+ }
+ else
+ {
+ SQL_FetchRow(results[0]);
+ int rec_distance = SQL_FetchInt(results[0], JumpstatDB_Lookup_Distance);
+ int rec_block = SQL_FetchInt(results[0], JumpstatDB_Lookup_Block);
+
+ if (rec_block == 0)
+ {
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Distance] = rec_distance;
+ }
+ else
+ {
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Block] = rec_block;
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_BlockDistance] = rec_distance;
+ }
+
+ if (block < rec_block || block == rec_block && distance < rec_distance)
+ {
+ delete data;
+ return;
+ }
+
+ if (rows < GOKZ_DB_JS_MAX_JUMPS_PER_PLAYER)
+ {
+ FormatEx(query, sizeof(query), sql_jumpstats_insert, steamid, jumpType, mode, distance, block > 0, block, strafes, sync, pre, max, airtime);
+ }
+ else
+ {
+ for (int i = 1; i < GOKZ_DB_JS_MAX_JUMPS_PER_PLAYER; i++)
+ {
+ SQL_FetchRow(results[0]);
+ }
+ int min_rec_id = SQL_FetchInt(results[0], JumpstatDB_Lookup_JumpID);
+ FormatEx(query, sizeof(query), sql_jumpstats_update, steamid, jumpType, mode, distance, block > 0, block, strafes, sync, pre, max, airtime, min_rec_id);
+ }
+
+ }
+
+ Transaction txn = SQL_CreateTransaction();
+ txn.AddQuery(query);
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SaveJSRecord, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_SaveJSRecord(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = data.ReadCell();
+ data.ReadCell();
+ int jumpType = data.ReadCell();
+ int mode = data.ReadCell();
+ int distance = data.ReadCell();
+ int block = data.ReadCell();
+ int strafes = data.ReadCell();
+ int sync = data.ReadCell();
+ int pre = data.ReadCell();
+ int max = data.ReadCell();
+ int airtime = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client) || GOKZ_JS_GetOption(client, JSOption_JumpstatsMaster) == JSToggleOption_Disabled)
+ {
+ return;
+ }
+
+ float distanceFloat = float(distance) / GOKZ_DB_JS_DISTANCE_PRECISION;
+ float syncFloat = float(sync) / GOKZ_DB_JS_SYNC_PRECISION;
+ float preFloat = float(pre) / GOKZ_DB_JS_PRE_PRECISION;
+ float maxFloat = float(max) / GOKZ_DB_JS_MAX_PRECISION;
+
+ if (block == 0)
+ {
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Distance] = distance;
+ GOKZ_PrintToChat(client, true, "%t", "Jump Record",
+ client,
+ gC_JumpTypes[jumpType],
+ distanceFloat,
+ gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_Block] = block;
+ gI_PBJSCache[client][mode][jumpType][JumpstatDB_Cache_BlockDistance] = distance;
+ GOKZ_PrintToChat(client, true, "%t", "Block Jump Record",
+ client,
+ block,
+ gC_JumpTypes[jumpType],
+ distanceFloat,
+ gC_ModeNamesShort[mode],
+ block);
+ }
+
+ Call_OnJumpstatPB(client, jumpType, mode, distanceFloat, block, strafes, syncFloat, preFloat, maxFloat, airtime);
+}
+
+public void DB_DeleteBestJump(int client, int steamAccountID, int jumpType, int mode, int isBlock)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console
+ data.WriteCell(steamAccountID);
+ data.WriteCell(jumpType);
+ data.WriteCell(mode);
+ data.WriteCell(isBlock);
+
+ char query[1024];
+
+ FormatEx(query, sizeof(query), sql_jumpstats_deleterecord, steamAccountID, jumpType, mode, isBlock);
+
+ Transaction txn = SQL_CreateTransaction();
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_BestJumpDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_BestJumpDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ char blockString[16] = "";
+
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int steamAccountID = data.ReadCell();
+ int jumpType = data.ReadCell();
+ int mode = data.ReadCell();
+ bool isBlock = data.ReadCell() == 1;
+ delete data;
+
+ if (isBlock)
+ {
+ FormatEx(blockString, sizeof(blockString), "%T ", "Block", client);
+ }
+
+ ClearCache(client);
+
+ GOKZ_PrintToChatAndLog(client, true, "%t", "Best Jump Deleted",
+ gC_ModeNames[mode],
+ blockString,
+ gC_JumpTypes[jumpType],
+ steamAccountID & 1,
+ steamAccountID >> 1);
+}
+
+public void DB_DeleteAllJumps(int client, int steamAccountID)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console
+ data.WriteCell(steamAccountID);
+
+ char query[1024];
+
+ FormatEx(query, sizeof(query), sql_jumpstats_deleteallrecords, steamAccountID);
+
+ Transaction txn = SQL_CreateTransaction();
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_AllJumpsDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_AllJumpsDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int steamAccountID = data.ReadCell();
+ delete data;
+
+ ClearCache(client);
+
+ GOKZ_PrintToChatAndLog(client, true, "%t", "All Jumps Deleted",
+ steamAccountID & 1,
+ steamAccountID >> 1);
+}
+
+public void DB_DeleteJump(int client, int jumpID)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console
+ data.WriteCell(jumpID);
+
+ char query[1024];
+ FormatEx(query, sizeof(query), sql_jumpstats_deletejump, jumpID);
+
+ Transaction txn = SQL_CreateTransaction();
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_JumpDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_JumpDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int jumpID = data.ReadCell();
+ delete data;
+
+ GOKZ_PrintToChatAndLog(client, true, "%t", "Jump Deleted",
+ jumpID);
+}
diff --git a/sourcemod/scripting/gokz-localdb/db/save_time.sp b/sourcemod/scripting/gokz-localdb/db/save_time.sp
new file mode 100644
index 0000000..84589a5
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/save_time.sp
@@ -0,0 +1,83 @@
+/*
+ Inserts the player's time into the database.
+*/
+
+
+
+void DB_SaveTime(int client, int course, int mode, int style, float runTime, int teleportsUsed)
+{
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+
+ char query[1024];
+ int steamID = GetSteamAccountID(client);
+ int mapID = GOKZ_DB_GetCurrentMapID();
+ int runTimeMS = GOKZ_DB_TimeFloatToInt(runTime);
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(steamID);
+ data.WriteCell(mapID);
+ data.WriteCell(course);
+ data.WriteCell(mode);
+ data.WriteCell(style);
+ data.WriteCell(runTimeMS);
+ data.WriteCell(teleportsUsed);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Save runTime to DB
+ FormatEx(query, sizeof(query), sql_times_insert, steamID, mode, style, runTimeMS, teleportsUsed, mapID, course);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SaveTime, DB_TxnFailure_Generic_DataPack, data, DBPrio_Normal);
+}
+
+public void DB_TxnSuccess_SaveTime(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int steamID = data.ReadCell();
+ int mapID = data.ReadCell();
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ int style = data.ReadCell();
+ int runTimeMS = data.ReadCell();
+ int teleportsUsed = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ Call_OnTimeInserted(client, steamID, mapID, course, mode, style, runTimeMS, teleportsUsed);
+}
+
+public void DB_DeleteTime(int client, int timeID)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console
+ data.WriteCell(timeID);
+
+ char query[1024];
+ FormatEx(query, sizeof(query), sql_times_delete, timeID);
+
+ Transaction txn = SQL_CreateTransaction();
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_TimeDeleted, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_TimeDeleted(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int timeID = data.ReadCell();
+ delete data;
+
+ GOKZ_PrintToChatAndLog(client, true, "%t", "Time Deleted",
+ timeID);
+}
diff --git a/sourcemod/scripting/gokz-localdb/db/set_cheater.sp b/sourcemod/scripting/gokz-localdb/db/set_cheater.sp
new file mode 100644
index 0000000..c63f161
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/set_cheater.sp
@@ -0,0 +1,64 @@
+/*
+ Sets whether player is a cheater in the database.
+*/
+
+
+
+void DB_SetCheater(int cheaterClient, bool cheater)
+{
+ if (gB_Cheater[cheaterClient] == cheater)
+ {
+ return;
+ }
+
+ gB_Cheater[cheaterClient] = cheater;
+
+ DataPack data = new DataPack();
+ data.WriteCell(-1);
+ data.WriteCell(GetSteamAccountID(cheaterClient));
+ data.WriteCell(cheater);
+
+ char query[128];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ FormatEx(query, sizeof(query), sql_players_set_cheater, cheater ? 1 : 0, GetSteamAccountID(cheaterClient));
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetCheater, DB_TxnFailure_Generic_DataPack, data, DBPrio_High);
+}
+
+void DB_SetCheaterSteamID(int client, int cheaterSteamID, bool cheater)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(client == 0 ? -1 : GetClientUserId(client)); // -1 if called from server console
+ data.WriteCell(cheaterSteamID);
+ data.WriteCell(cheater);
+
+ char query[128];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ FormatEx(query, sizeof(query), sql_players_set_cheater, cheater ? 1 : 0, cheaterSteamID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetCheater, DB_TxnFailure_Generic_DataPack, data, DBPrio_High);
+}
+
+public void DB_TxnSuccess_SetCheater(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int steamID = data.ReadCell();
+ bool cheater = view_as<bool>(data.ReadCell());
+ delete data;
+
+ if (cheater)
+ {
+ GOKZ_PrintToChatAndLog(client, true, "%t", "Set Cheater", steamID & 1, steamID >> 1);
+ }
+ else
+ {
+ GOKZ_PrintToChatAndLog(client, true, "%t", "Set Not Cheater", steamID & 1, steamID >> 1);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/setup_client.sp b/sourcemod/scripting/gokz-localdb/db/setup_client.sp
new file mode 100644
index 0000000..848be87
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/setup_client.sp
@@ -0,0 +1,99 @@
+/*
+ Inserts the player into the database, or else updates their information.
+*/
+
+
+
+void DB_SetupClient(int client)
+{
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+
+ // Setup Client Step 1 - Upsert them into Players Table
+ char query[1024], name[MAX_NAME_LENGTH], nameEscaped[MAX_NAME_LENGTH * 2 + 1], clientIP[16], country[45];
+
+ int steamID = GetSteamAccountID(client);
+ if (!GetClientName(client, name, MAX_NAME_LENGTH))
+ {
+ LogMessage("Couldn't get name of %L.", client);
+ name = "Unknown";
+ }
+ SQL_EscapeString(gH_DB, name, nameEscaped, MAX_NAME_LENGTH * 2 + 1);
+ if (!GetClientIP(client, clientIP, sizeof(clientIP)))
+ {
+ LogMessage("Couldn't get IP of %L.", client);
+ clientIP = "Unknown";
+ }
+ if (!GeoipCountry(clientIP, country, sizeof(country)))
+ {
+ LogMessage("Couldn't get country of %L (%s).", client, clientIP);
+ country = "Unknown";
+ }
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(steamID);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Insert/Update player into Players table
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ // UPDATE OR IGNORE
+ FormatEx(query, sizeof(query), sqlite_players_update, nameEscaped, country, clientIP, steamID);
+ txn.AddQuery(query);
+ // INSERT OR IGNORE
+ FormatEx(query, sizeof(query), sqlite_players_insert, nameEscaped, country, clientIP, steamID);
+ txn.AddQuery(query);
+ }
+ case DatabaseType_MySQL:
+ {
+ // INSERT ... ON DUPLICATE KEY ...
+ FormatEx(query, sizeof(query), mysql_players_upsert, nameEscaped, country, clientIP, steamID);
+ txn.AddQuery(query);
+ }
+ }
+
+ FormatEx(query, sizeof(query), sql_players_get_cheater, steamID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetupClient, DB_TxnFailure_Generic_DataPack, data, DBPrio_High);
+}
+
+public void DB_TxnSuccess_SetupClient(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int steamID = data.ReadCell();
+ delete data;
+
+ if (client == 0 || !IsClientAuthorized(client))
+ {
+ return;
+ }
+
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ if (SQL_FetchRow(results[2]))
+ {
+ gB_Cheater[client] = SQL_FetchInt(results[2], 0) == 1;
+ }
+ }
+ case DatabaseType_MySQL:
+ {
+ if (SQL_FetchRow(results[1]))
+ {
+ gB_Cheater[client] = SQL_FetchInt(results[1], 0) == 1;
+ }
+ }
+ }
+
+ gB_ClientSetUp[client] = true;
+ Call_OnClientSetup(client, steamID, gB_Cheater[client]);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/setup_database.sp b/sourcemod/scripting/gokz-localdb/db/setup_database.sp
new file mode 100644
index 0000000..4965541
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/setup_database.sp
@@ -0,0 +1,34 @@
+/*
+ Set up the connection to the local database.
+*/
+
+
+
+void DB_SetupDatabase()
+{
+ char error[255];
+ gH_DB = SQL_Connect("gokz", true, error, sizeof(error));
+ if (gH_DB == null)
+ {
+ SetFailState("Database connection failed. Error: \"%s\".", error);
+ }
+
+ char databaseType[8];
+ SQL_ReadDriver(gH_DB, databaseType, sizeof(databaseType));
+ if (strcmp(databaseType, "sqlite", false) == 0)
+ {
+ g_DBType = DatabaseType_SQLite;
+ }
+ else if (strcmp(databaseType, "mysql", false) == 0)
+ {
+ g_DBType = DatabaseType_MySQL;
+ }
+ else
+ {
+ SetFailState("Incompatible database driver. Use SQLite or MySQL.");
+ }
+
+ DB_CreateTables();
+
+ Call_OnDatabaseConnect();
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/setup_map.sp b/sourcemod/scripting/gokz-localdb/db/setup_map.sp
new file mode 100644
index 0000000..a02e2d2
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/setup_map.sp
@@ -0,0 +1,71 @@
+/*
+ Inserts the map information into the database.
+ Retrieves the MapID of the map and stores it in a global variable.
+*/
+
+
+
+void DB_SetupMap()
+{
+ gB_MapSetUp = false;
+
+ char query[1024];
+
+ char map[PLATFORM_MAX_PATH];
+ GetCurrentMapDisplayName(map, sizeof(map));
+
+ char escapedMap[PLATFORM_MAX_PATH * 2 + 1];
+ SQL_EscapeString(gH_DB, map, escapedMap, sizeof(escapedMap));
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Insert/Update map into database
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ // UPDATE OR IGNORE
+ FormatEx(query, sizeof(query), sqlite_maps_update, escapedMap);
+ txn.AddQuery(query);
+ // INSERT OR IGNORE
+ FormatEx(query, sizeof(query), sqlite_maps_insert, escapedMap);
+ txn.AddQuery(query);
+ }
+ case DatabaseType_MySQL:
+ {
+ // INSERT ... ON DUPLICATE KEY ...
+ FormatEx(query, sizeof(query), mysql_maps_upsert, escapedMap);
+ txn.AddQuery(query);
+ }
+ }
+ // Retrieve mapID of map name
+ FormatEx(query, sizeof(query), sql_maps_findid, escapedMap, escapedMap);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SetupMap, DB_TxnFailure_Generic, 0, DBPrio_High);
+}
+
+public void DB_TxnSuccess_SetupMap(Handle db, any data, int numQueries, Handle[] results, any[] queryData)
+{
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ if (SQL_FetchRow(results[2]))
+ {
+ gI_DBCurrentMapID = SQL_FetchInt(results[2], 0);
+ gB_MapSetUp = true;
+ Call_OnMapSetup();
+ }
+ }
+ case DatabaseType_MySQL:
+ {
+ if (SQL_FetchRow(results[1]))
+ {
+ gI_DBCurrentMapID = SQL_FetchInt(results[1], 0);
+ gB_MapSetUp = true;
+ Call_OnMapSetup();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp b/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp
new file mode 100644
index 0000000..69bb89e
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/setup_map_courses.sp
@@ -0,0 +1,45 @@
+/*
+ Inserts the map's courses into the database.
+*/
+
+
+
+void DB_SetupMapCourses()
+{
+ char query[512];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ if (!GOKZ_GetCourseRegistered(course))
+ {
+ continue;
+ }
+
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:FormatEx(query, sizeof(query), sqlite_mapcourses_insert, gI_DBCurrentMapID, course);
+ case DatabaseType_MySQL:FormatEx(query, sizeof(query), mysql_mapcourses_insert, gI_DBCurrentMapID, course);
+ }
+ txn.AddQuery(query);
+ }
+
+ SQL_ExecuteTransaction(gH_DB, txn, INVALID_FUNCTION, DB_TxnFailure_Generic, _, DBPrio_High);
+}
+
+void DB_SetupMapCourse(int course)
+{
+ char query[512];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:FormatEx(query, sizeof(query), sqlite_mapcourses_insert, gI_DBCurrentMapID, course);
+ case DatabaseType_MySQL:FormatEx(query, sizeof(query), mysql_mapcourses_insert, gI_DBCurrentMapID, course);
+ }
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, INVALID_FUNCTION, DB_TxnFailure_Generic, _, DBPrio_High);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localdb/db/sql.sp b/sourcemod/scripting/gokz-localdb/db/sql.sp
new file mode 100644
index 0000000..46ea5e3
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/sql.sp
@@ -0,0 +1,406 @@
+/*
+ SQL query templates.
+*/
+
+
+
+// =====[ PLAYERS ]=====
+
+char sqlite_players_create[] = "\
+CREATE TABLE IF NOT EXISTS Players ( \
+ SteamID32 INTEGER NOT NULL, \
+ Alias TEXT, \
+ Country TEXT, \
+ IP TEXT, \
+ Cheater INTEGER NOT NULL DEFAULT '0', \
+ LastPlayed TIMESTAMP NULL DEFAULT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Player PRIMARY KEY (SteamID32))";
+
+char mysql_players_create[] = "\
+CREATE TABLE IF NOT EXISTS Players ( \
+ SteamID32 INTEGER UNSIGNED NOT NULL, \
+ Alias VARCHAR(32), \
+ Country VARCHAR(45), \
+ IP VARCHAR(15), \
+ Cheater TINYINT UNSIGNED NOT NULL DEFAULT '0', \
+ LastPlayed TIMESTAMP NULL DEFAULT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Player PRIMARY KEY (SteamID32))";
+
+char sqlite_players_insert[] = "\
+INSERT OR IGNORE INTO Players (Alias, Country, IP, SteamID32, LastPlayed) \
+ VALUES ('%s', '%s', '%s', %d, CURRENT_TIMESTAMP)";
+
+char sqlite_players_update[] = "\
+UPDATE OR IGNORE Players \
+ SET Alias='%s', Country='%s', IP='%s', LastPlayed=CURRENT_TIMESTAMP \
+ WHERE SteamID32=%d";
+
+char mysql_players_upsert[] = "\
+INSERT INTO Players (Alias, Country, IP, SteamID32, LastPlayed) \
+ VALUES ('%s', '%s', '%s', %d, CURRENT_TIMESTAMP) \
+ ON DUPLICATE KEY UPDATE \
+ SteamID32=VALUES(SteamID32), Alias=VALUES(Alias), Country=VALUES(Country), \
+ IP=VALUES(IP), LastPlayed=VALUES(LastPlayed)";
+
+char sql_players_get_cheater[] = "\
+SELECT Cheater \
+ FROM Players \
+ WHERE SteamID32=%d";
+
+char sql_players_set_cheater[] = "\
+UPDATE Players \
+ SET Cheater=%d \
+ WHERE SteamID32=%d";
+
+
+
+// =====[ MAPS ]=====
+
+char sqlite_maps_create[] = "\
+CREATE TABLE IF NOT EXISTS Maps ( \
+ MapID INTEGER NOT NULL, \
+ Name VARCHAR(32) NOT NULL UNIQUE, \
+ LastPlayed TIMESTAMP NULL DEFAULT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Maps PRIMARY KEY (MapID))";
+
+char mysql_maps_create[] = "\
+CREATE TABLE IF NOT EXISTS Maps ( \
+ MapID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \
+ Name VARCHAR(32) NOT NULL UNIQUE, \
+ LastPlayed TIMESTAMP NULL DEFAULT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Maps PRIMARY KEY (MapID))";
+
+char sqlite_maps_insert[] = "\
+INSERT OR IGNORE INTO Maps (Name, LastPlayed) \
+ VALUES ('%s', CURRENT_TIMESTAMP)";
+
+char sqlite_maps_update[] = "\
+UPDATE OR IGNORE Maps \
+ SET LastPlayed=CURRENT_TIMESTAMP \
+ WHERE Name='%s'";
+
+char mysql_maps_upsert[] = "\
+INSERT INTO Maps (Name, LastPlayed) \
+ VALUES ('%s', CURRENT_TIMESTAMP) \
+ ON DUPLICATE KEY UPDATE \
+ LastPlayed=CURRENT_TIMESTAMP";
+
+char sql_maps_findid[] = "\
+SELECT MapID, Name \
+ FROM Maps \
+ WHERE Name LIKE '%%%s%%' \
+ ORDER BY (Name='%s') DESC, LENGTH(Name) \
+ LIMIT 1";
+
+
+
+// =====[ MAPCOURSES ]=====
+
+char sqlite_mapcourses_create[] = "\
+CREATE TABLE IF NOT EXISTS MapCourses ( \
+ MapCourseID INTEGER NOT NULL, \
+ MapID INTEGER NOT NULL, \
+ Course INTEGER NOT NULL, \
+ Created INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_MapCourses PRIMARY KEY (MapCourseID), \
+ CONSTRAINT UQ_MapCourses_MapIDCourse UNIQUE (MapID, Course), \
+ CONSTRAINT FK_MapCourses_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char mysql_mapcourses_create[] = "\
+CREATE TABLE IF NOT EXISTS MapCourses ( \
+ MapCourseID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \
+ MapID INTEGER UNSIGNED NOT NULL, \
+ Course INTEGER UNSIGNED NOT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_MapCourses PRIMARY KEY (MapCourseID), \
+ CONSTRAINT UQ_MapCourses_MapIDCourse UNIQUE (MapID, Course), \
+ CONSTRAINT FK_MapCourses_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char sqlite_mapcourses_insert[] = "\
+INSERT OR IGNORE INTO MapCourses (MapID, Course) \
+ VALUES (%d, %d)";
+
+char mysql_mapcourses_insert[] = "\
+INSERT IGNORE INTO MapCourses (MapID, Course) \
+ VALUES (%d, %d)";
+
+
+
+// =====[ TIMES ]=====
+
+char sqlite_times_create[] = "\
+CREATE TABLE IF NOT EXISTS Times ( \
+ TimeID INTEGER NOT NULL, \
+ SteamID32 INTEGER NOT NULL, \
+ MapCourseID INTEGER NOT NULL, \
+ Mode INTEGER NOT NULL, \
+ Style INTEGER NOT NULL, \
+ RunTime INTEGER NOT NULL, \
+ Teleports INTEGER NOT NULL, \
+ Created INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Times PRIMARY KEY (TimeID), \
+ CONSTRAINT FK_Times_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \
+ ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT FK_Times_MapCourseID \
+ FOREIGN KEY (MapCourseID) REFERENCES MapCourses(MapCourseID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char mysql_times_create[] = "\
+CREATE TABLE IF NOT EXISTS Times ( \
+ TimeID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \
+ SteamID32 INTEGER UNSIGNED NOT NULL, \
+ MapCourseID INTEGER UNSIGNED NOT NULL, \
+ Mode TINYINT UNSIGNED NOT NULL, \
+ Style TINYINT UNSIGNED NOT NULL, \
+ RunTime INTEGER UNSIGNED NOT NULL, \
+ Teleports SMALLINT UNSIGNED NOT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Times PRIMARY KEY (TimeID), \
+ CONSTRAINT FK_Times_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \
+ ON UPDATE CASCADE ON DELETE CASCADE, \
+ CONSTRAINT FK_Times_MapCourseID FOREIGN KEY (MapCourseID) REFERENCES MapCourses(MapCourseID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char sql_times_insert[] = "\
+INSERT INTO Times (SteamID32, MapCourseID, Mode, Style, RunTime, Teleports) \
+ SELECT %d, MapCourseID, %d, %d, %d, %d \
+ FROM MapCourses \
+ WHERE MapID=%d AND Course=%d";
+
+char sql_times_delete[] = "\
+DELETE FROM Times \
+ WHERE TimeID=%d";
+
+
+
+// =====[ JUMPSTATS ]=====
+
+char sqlite_jumpstats_create[] = "\
+CREATE TABLE IF NOT EXISTS Jumpstats ( \
+ JumpID INTEGER NOT NULL, \
+ SteamID32 INTEGER NOT NULL, \
+ JumpType INTEGER NOT NULL, \
+ Mode INTEGER NOT NULL, \
+ Distance INTEGER NOT NULL, \
+ IsBlockJump INTEGER NOT NULL, \
+ Block INTEGER NOT NULL, \
+ Strafes INTEGER NOT NULL, \
+ Sync INTEGER NOT NULL, \
+ Pre INTEGER NOT NULL, \
+ Max INTEGER NOT NULL, \
+ Airtime INTEGER NOT NULL, \
+ Created INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Jumpstats PRIMARY KEY (JumpID), \
+ CONSTRAINT FK_Jumpstats_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char mysql_jumpstats_create[] = "\
+CREATE TABLE IF NOT EXISTS Jumpstats ( \
+ JumpID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, \
+ SteamID32 INTEGER UNSIGNED NOT NULL, \
+ JumpType TINYINT UNSIGNED NOT NULL, \
+ Mode TINYINT UNSIGNED NOT NULL, \
+ Distance INTEGER UNSIGNED NOT NULL, \
+ IsBlockJump TINYINT UNSIGNED NOT NULL, \
+ Block SMALLINT UNSIGNED NOT NULL, \
+ Strafes INTEGER UNSIGNED NOT NULL, \
+ Sync INTEGER UNSIGNED NOT NULL, \
+ Pre INTEGER UNSIGNED NOT NULL, \
+ Max INTEGER UNSIGNED NOT NULL, \
+ Airtime INTEGER UNSIGNED NOT NULL, \
+ Created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \
+ CONSTRAINT PK_Jumpstats PRIMARY KEY (JumpID), \
+ CONSTRAINT FK_Jumpstats_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char sql_jumpstats_insert[] = "\
+INSERT INTO Jumpstats (SteamID32, JumpType, Mode, Distance, IsBlockJump, Block, Strafes, Sync, Pre, Max, Airtime) \
+ VALUES (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)";
+
+char sql_jumpstats_update[] = "\
+UPDATE Jumpstats \
+ SET \
+ SteamID32=%d, \
+ JumpType=%d, \
+ Mode=%d, \
+ Distance=%d, \
+ IsBlockJump=%d, \
+ Block=%d, \
+ Strafes=%d, \
+ Sync=%d, \
+ Pre=%d, \
+ Max=%d, \
+ Airtime=%d \
+ WHERE \
+ JumpID=%d";
+
+char sql_jumpstats_getrecord[] = "\
+SELECT JumpID, Distance, Block \
+ FROM \
+ Jumpstats \
+ WHERE \
+ SteamID32=%d AND \
+ JumpType=%d AND \
+ Mode=%d AND \
+ IsBlockJump=%d \
+ ORDER BY Block DESC, Distance DESC";
+
+char sql_jumpstats_deleterecord[] = "\
+DELETE \
+ FROM \
+ Jumpstats \
+ WHERE \
+ JumpID = \
+ ( SELECT * FROM ( \
+ SELECT JumpID \
+ FROM \
+ Jumpstats \
+ WHERE \
+ SteamID32=%d AND \
+ JumpType=%d AND \
+ Mode=%d AND \
+ IsBlockJump=%d \
+ ORDER BY Block DESC, Distance DESC \
+ LIMIT 1 \
+ ) AS tmp \
+ )";
+
+char sql_jumpstats_deleteallrecords[] = "\
+DELETE \
+ FROM \
+ Jumpstats \
+ WHERE \
+ SteamID32 = %d;";
+
+char sql_jumpstats_deletejump[] = "\
+DELETE \
+ FROM \
+ Jumpstats \
+ WHERE \
+ JumpID = %d;";
+
+char sql_jumpstats_getpbs[] = "\
+SELECT MAX(Distance), Mode, JumpType \
+ FROM \
+ Jumpstats \
+ WHERE \
+ SteamID32=%d \
+ GROUP BY \
+ Mode, JumpType";
+
+char sql_jumpstats_getblockpbs[] = "\
+SELECT MAX(js.Distance), js.Mode, js.JumpType, js.Block \
+ FROM \
+ Jumpstats js \
+ INNER JOIN \
+ ( \
+ SELECT Mode, JumpType, MAX(BLOCK) Block \
+ FROM \
+ Jumpstats \
+ WHERE \
+ IsBlockJump=1 AND \
+ SteamID32=%d \
+ GROUP BY \
+ Mode, JumpType \
+ ) pb \
+ ON \
+ js.Mode=pb.Mode AND \
+ js.JumpType=pb.JumpType AND \
+ js.Block=pb.Block \
+ WHERE \
+ js.SteamID32=%d \
+ GROUP BY \
+ js.Mode, js.JumpType, js.Block";
+
+
+
+// =====[ VB POSITIONS ]=====
+
+char sqlite_vbpos_create[] = "\
+CREATE TABLE IF NOT EXISTS VBPosition ( \
+ SteamID32 INTEGER NOT NULL, \
+ MapID INTEGER NOT NULL, \
+ X REAL NOT NULL, \
+ Y REAL NOT NULL, \
+ Z REAL NOT NULL, \
+ Course INTEGER NOT NULL, \
+ IsStart INTEGER NOT NULL, \
+ CONSTRAINT PK_VBPosition PRIMARY KEY (SteamID32, MapID, IsStart), \
+ CONSTRAINT FK_VBPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32), \
+ CONSTRAINT FK_VBPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char mysql_vbpos_create[] = "\
+CREATE TABLE IF NOT EXISTS VBPosition ( \
+ SteamID32 INTEGER UNSIGNED NOT NULL, \
+ MapID INTEGER UNSIGNED NOT NULL, \
+ X REAL NOT NULL, \
+ Y REAL NOT NULL, \
+ Z REAL NOT NULL, \
+ Course INTEGER NOT NULL, \
+ IsStart INTEGER NOT NULL, \
+ CONSTRAINT PK_VBPosition PRIMARY KEY (SteamID32, MapID, IsStart), \
+ CONSTRAINT FK_VBPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32), \
+ CONSTRAINT FK_VBPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char sql_vbpos_upsert[] = "\
+REPLACE INTO VBPosition (SteamID32, MapID, X, Y, Z, Course, IsStart) \
+ VALUES (%d, %d, %f, %f, %f, %d, %d)";
+
+char sql_vbpos_get[] = "\
+SELECT SteamID32, MapID, Course, IsStart, X, Y, Z \
+ FROM \
+ VBPosition \
+ WHERE \
+ SteamID32 = %d AND \
+ MapID = %d";
+
+
+
+// =====[ START POSITIONS ]=====
+
+char sqlite_startpos_create[] = "\
+CREATE TABLE IF NOT EXISTS StartPosition ( \
+ SteamID32 INTEGER NOT NULL, \
+ MapID INTEGER NOT NULL, \
+ X REAL NOT NULL, \
+ Y REAL NOT NULL, \
+ Z REAL NOT NULL, \
+ Angle0 REAL NOT NULL, \
+ Angle1 REAL NOT NULL, \
+ CONSTRAINT PK_StartPosition PRIMARY KEY (SteamID32, MapID), \
+ CONSTRAINT FK_StartPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32) \
+ CONSTRAINT FK_StartPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char mysql_startpos_create[] = "\
+CREATE TABLE IF NOT EXISTS StartPosition ( \
+ SteamID32 INTEGER UNSIGNED NOT NULL, \
+ MapID INTEGER UNSIGNED NOT NULL, \
+ X REAL NOT NULL, \
+ Y REAL NOT NULL, \
+ Z REAL NOT NULL, \
+ Angle0 REAL NOT NULL, \
+ Angle1 REAL NOT NULL, \
+ CONSTRAINT PK_StartPosition PRIMARY KEY (SteamID32, MapID), \
+ CONSTRAINT FK_StartPosition_SteamID32 FOREIGN KEY (SteamID32) REFERENCES Players(SteamID32), \
+ CONSTRAINT FK_StartPosition_MapID FOREIGN KEY (MapID) REFERENCES Maps(MapID) \
+ ON UPDATE CASCADE ON DELETE CASCADE)";
+
+char sql_startpos_upsert[] = "\
+REPLACE INTO StartPosition (SteamID32, MapID, X, Y, Z, Angle0, Angle1) \
+ VALUES (%d, %d, %f, %f, %f, %f, %f)";
+
+char sql_startpos_get[] = "\
+SELECT SteamID32, MapID, X, Y, Z, Angle0, Angle1 \
+ FROM \
+ StartPosition \
+ WHERE \
+ SteamID32 = %d AND \
+ MapID = %d";
diff --git a/sourcemod/scripting/gokz-localdb/db/timer_setup.sp b/sourcemod/scripting/gokz-localdb/db/timer_setup.sp
new file mode 100644
index 0000000..b123eeb
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/db/timer_setup.sp
@@ -0,0 +1,167 @@
+
+// ===== [ SAVE TIMER SETUP ] =====
+
+void DB_SaveTimerSetup(int client)
+{
+ bool txnHasQuery = false;
+ int course;
+ float position[3], angles[3];
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ int steamid = GetSteamAccountID(client);
+ DataPack data = new DataPack();
+
+ data.WriteCell(client);
+ data.WriteCell(steamid);
+
+ char query[1024];
+ Transaction txn = SQL_CreateTransaction();
+
+ if (GOKZ_GetStartPosition(client, position, angles) == StartPositionType_Custom)
+ {
+ FormatEx(query, sizeof(query), sql_startpos_upsert, steamid, gI_DBCurrentMapID, position[0], position[1], position[2], angles[0], angles[1]);
+ txn.AddQuery(query);
+ txnHasQuery = true;
+ }
+
+ course = GOKZ_GetVirtualButtonPosition(client, position, true);
+ if (course != -1)
+ {
+ FormatEx(query, sizeof(query), sql_vbpos_upsert, steamid, gI_DBCurrentMapID, position[0], position[1], position[2], course, 1);
+ txn.AddQuery(query);
+ txnHasQuery = true;
+ }
+
+ course = GOKZ_GetVirtualButtonPosition(client, position, false);
+ if (course != -1)
+ {
+ FormatEx(query, sizeof(query), sql_vbpos_upsert, steamid, gI_DBCurrentMapID, position[0], position[1], position[2], course, 0);
+ txn.AddQuery(query);
+ txnHasQuery = true;
+ }
+
+ if (txnHasQuery)
+ {
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_SaveTimerSetup, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+ }
+ else
+ {
+ delete data;
+ delete txn;
+ }
+}
+
+public void DB_TxnSuccess_SaveTimerSetup(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = data.ReadCell();
+ int steamid = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client) || steamid != GetSteamAccountID(client))
+ {
+ return;
+ }
+
+ GOKZ_PrintToChat(client, true, "%t", "Timer Setup Saved");
+}
+
+
+
+// ===== [ LOAD TIMER SETUP ] =====
+
+void DB_LoadTimerSetup(int client, bool doChatMessage = false)
+{
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ int steamid = GetSteamAccountID(client);
+
+ DataPack data = new DataPack();
+ data.WriteCell(client);
+ data.WriteCell(steamid);
+ data.WriteCell(doChatMessage);
+
+ char query[1024];
+ Transaction txn = SQL_CreateTransaction();
+
+ // Virtual Buttons
+ FormatEx(query, sizeof(query), sql_vbpos_get, steamid, gI_DBCurrentMapID);
+ txn.AddQuery(query);
+
+ // Start Position
+ FormatEx(query, sizeof(query), sql_startpos_get, steamid, gI_DBCurrentMapID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_LoadTimerSetup, DB_TxnFailure_Generic_DataPack, data, DBPrio_Normal);
+}
+
+public void DB_TxnSuccess_LoadTimerSetup(Handle db, DataPack data, int numQueries, DBResultSet[] results, any[] queryData)
+{
+ data.Reset();
+ int client = data.ReadCell();
+ int steamid = data.ReadCell();
+ bool doChatMessage = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client) || steamid != GetSteamAccountID(client))
+ {
+ return;
+ }
+
+ int course;
+ bool isStart, vbSetup = false;
+ float position[3], angles[3];
+
+ if (results[0].RowCount > 0 && results[0].FetchRow())
+ {
+ position[0] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionX);
+ position[1] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionY);
+ position[2] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionZ);
+ course = results[0].FetchInt(TimerSetupDB_GetVBPos_Course);
+ isStart = results[0].FetchInt(TimerSetupDB_GetVBPos_IsStart) == 1;
+
+ GOKZ_SetVirtualButtonPosition(client, position, course, isStart);
+ vbSetup = true;
+ }
+
+ if (results[0].RowCount > 1 && results[0].FetchRow())
+ {
+ position[0] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionX);
+ position[1] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionY);
+ position[2] = results[0].FetchFloat(TimerSetupDB_GetVBPos_PositionZ);
+ course = results[0].FetchInt(TimerSetupDB_GetVBPos_Course);
+ isStart = results[0].FetchInt(TimerSetupDB_GetVBPos_IsStart) == 1;
+
+ GOKZ_SetVirtualButtonPosition(client, position, course, isStart);
+ vbSetup = true;
+ }
+
+ if (results[1].RowCount > 0 && results[1].FetchRow())
+ {
+ position[0] = results[1].FetchFloat(TimerSetupDB_GetStartPos_PositionX);
+ position[1] = results[1].FetchFloat(TimerSetupDB_GetStartPos_PositionY);
+ position[2] = results[1].FetchFloat(TimerSetupDB_GetStartPos_PositionZ);
+ angles[0] = results[1].FetchFloat(TimerSetupDB_GetStartPos_Angle0);
+ angles[1] = results[1].FetchFloat(TimerSetupDB_GetStartPos_Angle1);
+ angles[2] = 0.0;
+
+ GOKZ_SetStartPosition(client, StartPositionType_Custom, position, angles);
+ }
+
+ if (vbSetup)
+ {
+ GOKZ_LockVirtualButtons(client);
+ }
+
+ if (doChatMessage)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Setup Loaded");
+ }
+}
diff --git a/sourcemod/scripting/gokz-localdb/options.sp b/sourcemod/scripting/gokz-localdb/options.sp
new file mode 100644
index 0000000..2a8240a
--- /dev/null
+++ b/sourcemod/scripting/gokz-localdb/options.sp
@@ -0,0 +1,90 @@
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void RegisterOptions()
+{
+ for (DBOption option; option < DBOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_DBOptionNames[option], gC_DBOptionDescriptions[option],
+ OptionType_Int, gI_DBOptionDefaultValues[option], 0, gI_DBOptionCounts[option] - 1);
+ }
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatGeneral;
+TopMenuObject gTMO_ItemsDB[DBOPTION_COUNT];
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+ gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY);
+
+ for (int option = 0; option < view_as<int>(DBOPTION_COUNT); option++)
+ {
+ gTMO_ItemsDB[option] = gTM_Options.AddItem(gC_DBOptionNames[option], TopMenuHandler_DB, gTMO_CatGeneral);
+ }
+}
+
+public void TopMenuHandler_DB(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ DBOption option = DBOPTION_COUNT;
+ for (int i = 0; i < view_as<int>(DBOPTION_COUNT); i++)
+ {
+ if (topobj_id == gTMO_ItemsDB[i])
+ {
+ option = view_as<DBOption>(i);
+ break;
+ }
+ }
+
+ if (option == DBOPTION_COUNT)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case DBOption_AutoLoadTimerSetup:
+ {
+ FormatToggleableOptionDisplay(param, DBOption_AutoLoadTimerSetup, buffer, maxlength);
+ }
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_CycleOption(param, gC_DBOptionNames[option]);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
+void FormatToggleableOptionDisplay(int client, DBOption option, char[] buffer, int maxlength)
+{
+ if (GOKZ_GetOption(client, gC_DBOptionNames[option]) == DBOption_Disabled)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_DBOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_DBOptionPhrases[option], client,
+ "Options Menu - Enabled", client);
+ }
+}
diff --git a/sourcemod/scripting/gokz-localranks.sp b/sourcemod/scripting/gokz-localranks.sp
new file mode 100644
index 0000000..e2fd06d
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks.sp
@@ -0,0 +1,263 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdktools>
+
+#include <gokz/core>
+#include <gokz/localdb>
+#include <gokz/localranks>
+
+#include <sourcemod-colors>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/global>
+#include <gokz/jumpstats>
+#include <gokz/replays>
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Local Ranks",
+ author = "DanZay",
+ description = "Extends and provides in-game functionality for local database",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-localranks.txt"
+
+bool gB_GOKZGlobal;
+Database gH_DB = null;
+DatabaseType g_DBType = DatabaseType_None;
+bool gB_RecordExistsCache[GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT];
+float gF_RecordTimesCache[GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT];
+bool gB_RecordMissed[MAXPLAYERS + 1][TIMETYPE_COUNT];
+bool gB_PBExistsCache[MAXPLAYERS + 1][GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT];
+float gF_PBTimesCache[MAXPLAYERS + 1][GOKZ_MAX_COURSES][MODE_COUNT][TIMETYPE_COUNT];
+bool gB_PBMissed[MAXPLAYERS + 1][TIMETYPE_COUNT];
+char gC_BeatRecordSound[256];
+
+
+#include "gokz-localranks/api.sp"
+#include "gokz-localranks/commands.sp"
+#include "gokz-localranks/misc.sp"
+
+#include "gokz-localranks/db/sql.sp"
+#include "gokz-localranks/db/helpers.sp"
+#include "gokz-localranks/db/cache_pbs.sp"
+#include "gokz-localranks/db/cache_records.sp"
+#include "gokz-localranks/db/create_tables.sp"
+#include "gokz-localranks/db/get_completion.sp"
+#include "gokz-localranks/db/js_top.sp"
+#include "gokz-localranks/db/map_top.sp"
+#include "gokz-localranks/db/player_top.sp"
+#include "gokz-localranks/db/print_average.sp"
+#include "gokz-localranks/db/print_js.sp"
+#include "gokz-localranks/db/print_pbs.sp"
+#include "gokz-localranks/db/print_records.sp"
+#include "gokz-localranks/db/process_new_time.sp"
+#include "gokz-localranks/db/recent_records.sp"
+#include "gokz-localranks/db/update_ranked_map_pool.sp"
+#include "gokz-localranks/db/display_js.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-localranks");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-localranks.phrases");
+
+ CreateGlobalForwards();
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZGlobal = LibraryExists("gokz-global");
+
+ gH_DB = GOKZ_DB_GetDatabase();
+ if (gH_DB != null)
+ {
+ g_DBType = GOKZ_DB_GetDatabaseType();
+ DB_CreateTables();
+ CompletionMVPStarsUpdateAll();
+ }
+
+ if (GOKZ_DB_IsMapSetUp())
+ {
+ GOKZ_DB_OnMapSetup(GOKZ_DB_GetCurrentMapID());
+ }
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (GOKZ_DB_IsClientSetUp(i))
+ {
+ GOKZ_DB_OnClientSetup(i, GetSteamAccountID(i), GOKZ_DB_IsCheater(i));
+ }
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZGlobal = gB_GOKZGlobal || StrEqual(name, "gokz-global");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZGlobal = gB_GOKZGlobal && !StrEqual(name, "gokz-global");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+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])
+{
+ UpdateRecordMissed(client);
+ UpdatePBMissed(client);
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnTimerStart_Post(int client, int course)
+{
+ ResetRecordMissed(client);
+ ResetPBMissed(client);
+}
+
+public void GOKZ_DB_OnClientSetup(int client, int steamID, bool cheater)
+{
+ if (GOKZ_DB_IsMapSetUp())
+ {
+ DB_CachePBs(client, steamID);
+ CompletionMVPStarsUpdate(client);
+ }
+}
+
+public void GOKZ_DB_OnTimeInserted(int client, int steamID, int mapID, int course, int mode, int style, int runTimeMS, int teleportsUsed)
+{
+ if (GOKZ_DB_IsCheater(client))
+ {
+ DB_CachePBs(client, GetSteamAccountID(client));
+ }
+ else
+ {
+ DB_ProcessNewTime(client, steamID, mapID, course, mode, style, runTimeMS, teleportsUsed);
+ }
+}
+
+public void GOKZ_LR_OnTimeProcessed(
+ int client,
+ int steamID,
+ int mapID,
+ int course,
+ int mode,
+ int style,
+ float runTime,
+ int teleportsUsed,
+ bool firstTime,
+ float pbDiff,
+ int rank,
+ int maxRank,
+ bool firstTimePro,
+ float pbDiffPro,
+ int rankPro,
+ int maxRankPro)
+{
+ if (mapID != GOKZ_DB_GetCurrentMapID())
+ {
+ return;
+ }
+
+ AnnounceNewTime(client, course, mode, runTime, teleportsUsed, firstTime, pbDiff, rank, maxRank, firstTimePro, pbDiffPro, rankPro, maxRankPro);
+
+ if (mode == GOKZ_GetDefaultMode() && firstTimePro)
+ {
+ CompletionMVPStarsUpdate(client);
+ }
+
+ // If new PB, update PB cache
+ if (firstTime || firstTimePro || pbDiff < 0.0 || pbDiffPro < 0.0)
+ {
+ DB_CachePBs(client, GetSteamAccountID(client));
+ }
+}
+
+public void GOKZ_LR_OnNewRecord(int client, int steamID, int mapID, int course, int mode, int style, int recordType)
+{
+ if (mapID != GOKZ_DB_GetCurrentMapID())
+ {
+ return;
+ }
+
+ AnnounceNewRecord(client, course, mode, recordType);
+ DB_CacheRecords(mapID);
+}
+
+public void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType)
+{
+ DoPBMissedReport(client, pbTime, recordType);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ PrecacheAnnouncementSounds();
+}
+
+public void GOKZ_DB_OnDatabaseConnect(DatabaseType DBType)
+{
+ gH_DB = GOKZ_DB_GetDatabase();
+ g_DBType = DBType;
+ DB_CreateTables();
+ CompletionMVPStarsUpdateAll();
+}
+
+public void GOKZ_DB_OnMapSetup(int mapID)
+{
+ DB_CacheRecords(mapID);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GOKZ_DB_IsClientSetUp(client))
+ {
+ DB_CachePBs(client, GetSteamAccountID(client));
+ }
+ }
+}
+
+public Action GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed)
+{
+ if (GOKZ_DB_IsCheater(client))
+ {
+ return Plugin_Continue;
+ }
+
+ // Block timer end messages from GOKZ Core - this plugin handles them
+ return Plugin_Stop;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/api.sp b/sourcemod/scripting/gokz-localranks/api.sp
new file mode 100644
index 0000000..34b3ece
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/api.sp
@@ -0,0 +1,120 @@
+static GlobalForward H_OnTimeProcessed;
+static GlobalForward H_OnNewRecord;
+static GlobalForward H_OnRecordMissed;
+static GlobalForward H_OnPBMissed;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnTimeProcessed = new GlobalForward("GOKZ_LR_OnTimeProcessed", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Cell);
+ H_OnNewRecord = new GlobalForward("GOKZ_LR_OnNewRecord", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Float, Param_Cell, Param_Float, Param_Cell);
+ H_OnRecordMissed = new GlobalForward("GOKZ_LR_OnRecordMissed", ET_Ignore, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+ H_OnPBMissed = new GlobalForward("GOKZ_LR_OnPBMissed", ET_Ignore, Param_Cell, Param_Float, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+}
+
+void Call_OnTimeProcessed(
+ int client,
+ int steamID,
+ int mapID,
+ int course,
+ int mode,
+ int style,
+ float runTime,
+ int teleports,
+ bool firstTime,
+ float pbDiff,
+ int rank,
+ int maxRank,
+ bool firstTimePro,
+ float pbDiffPro,
+ int rankPro,
+ int maxRankPro)
+{
+ Call_StartForward(H_OnTimeProcessed);
+ Call_PushCell(client);
+ Call_PushCell(steamID);
+ Call_PushCell(mapID);
+ Call_PushCell(course);
+ Call_PushCell(mode);
+ Call_PushCell(style);
+ Call_PushFloat(runTime);
+ Call_PushCell(teleports);
+ Call_PushCell(firstTime);
+ Call_PushFloat(pbDiff);
+ Call_PushCell(rank);
+ Call_PushCell(maxRank);
+ Call_PushCell(firstTimePro);
+ Call_PushFloat(pbDiffPro);
+ Call_PushCell(rankPro);
+ Call_PushCell(maxRankPro);
+ Call_Finish();
+}
+
+void Call_OnNewRecord(int client, int steamID, int mapID, int course, int mode, int style, int recordType, float pbDiff, int teleportsUsed)
+{
+ Call_StartForward(H_OnNewRecord);
+ Call_PushCell(client);
+ Call_PushCell(steamID);
+ Call_PushCell(mapID);
+ Call_PushCell(course);
+ Call_PushCell(mode);
+ Call_PushCell(style);
+ Call_PushCell(recordType);
+ Call_PushFloat(pbDiff);
+ Call_PushCell(teleportsUsed);
+ Call_Finish();
+}
+
+void Call_OnRecordMissed(int client, float recordTime, int course, int mode, int style, int recordType)
+{
+ Call_StartForward(H_OnRecordMissed);
+ Call_PushCell(client);
+ Call_PushFloat(recordTime);
+ Call_PushCell(course);
+ Call_PushCell(mode);
+ Call_PushCell(style);
+ Call_PushCell(recordType);
+ Call_Finish();
+}
+
+void Call_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType)
+{
+ Call_StartForward(H_OnPBMissed);
+ Call_PushCell(client);
+ Call_PushFloat(pbTime);
+ Call_PushCell(course);
+ Call_PushCell(mode);
+ Call_PushCell(style);
+ Call_PushCell(recordType);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_LR_GetRecordMissed", Native_GetRecordMissed);
+ CreateNative("GOKZ_LR_GetPBMissed", Native_GetPBMissed);
+ CreateNative("GOKZ_LR_ReopenMapTopMenu", Native_ReopenMapTopMenu);
+}
+
+public int Native_GetRecordMissed(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_RecordMissed[GetNativeCell(1)][GetNativeCell(2)]);
+}
+
+public int Native_GetPBMissed(Handle plugin, int numParams)
+{
+ return view_as<int>(gB_PBMissed[GetNativeCell(1)][GetNativeCell(2)]);
+}
+
+public int Native_ReopenMapTopMenu(Handle plugin, int numParams)
+{
+ ReopenMapTopMenu(GetNativeCell(1));
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/commands.sp b/sourcemod/scripting/gokz-localranks/commands.sp
new file mode 100644
index 0000000..44063af
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/commands.sp
@@ -0,0 +1,506 @@
+static float lastCommandTime[MAXPLAYERS + 1];
+
+
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_top", CommandTop, "[KZ] Open a menu showing the top record holders.");
+ RegConsoleCmd("sm_maptop", CommandMapTop, "[KZ] Open a menu showing the top main course times of a map. Usage: !maptop <map>");
+ RegConsoleCmd("sm_bmaptop", CommandBMapTop, "[KZ] Open a menu showing the top bonus times of a map. Usage: !bmaptop <#bonus> <map>");
+ RegConsoleCmd("sm_bonustop", CommandBMapTop, "[KZ] Open a menu showing the top bonus times of a map. Usage: !bonustop <#bonus> <map>");
+ RegConsoleCmd("sm_btop", CommandBMapTop, "[KZ] Open a menu showing the top bonus times of a map. Usage: !btop <#bonus> <map>");
+ RegConsoleCmd("sm_pb", CommandPB, "[KZ] Show PB main course times and ranks in chat. Usage: !pb <map> <player>");
+ RegConsoleCmd("sm_bpb", CommandBPB, "[KZ] Show PB bonus times and ranks in chat. Usage: !bpb <#bonus> <map> <player>");
+ RegConsoleCmd("sm_wr", CommandWR, "[KZ] Show main course record times in chat. Usage: !wr <map>");
+ RegConsoleCmd("sm_bwr", CommandBWR, "[KZ] Show bonus record times in chat. Usage: !bwr <#bonus> <map>");
+ RegConsoleCmd("sm_avg", CommandAVG, "[KZ] Show the average main course run time in chat. Usage !avg <map>");
+ RegConsoleCmd("sm_bavg", CommandBAVG, "[KZ] Show the average bonus run time in chat. Usage !bavg <#bonus> <map>");
+ RegConsoleCmd("sm_pc", CommandPC, "[KZ] Show course completion in chat. Usage: !pc <player>");
+ RegConsoleCmd("sm_rr", CommandRecentRecords, "[KZ] Open a menu showing recently broken records.");
+ RegConsoleCmd("sm_latest", CommandRecentRecords, "[KZ] Open a menu showing recently broken records.");
+
+ RegConsoleCmd("sm_ljpb", CommandLJPB, "[KZ] Show PB Long Jump in chat. Usage: !ljpb <jumper>");
+ RegConsoleCmd("sm_bhpb", CommandBHPB, "[KZ] Show PB Bunnyhop in chat. Usage: !bhpb <jumper>");
+ RegConsoleCmd("sm_lbhpb", CommandLBHPB, "[KZ] Show PB Lowpre Bunnyhop in chat. Usage: !lbhpb <jumper>");
+ RegConsoleCmd("sm_mbhpb", CommandMBHPB, "[KZ] Show PB Multi Bunnyhop in chat. Usage: !mbhpb <jumper>");
+ RegConsoleCmd("sm_wjpb", CommandWJPB, "[KZ] Show PB Weird Jump in chat. Usage: !wjpb <jumper>");
+ RegConsoleCmd("sm_lwjpb", CommandLWJPB, "[KZ] Show PB Lowpre Weird Jump in chat. Usage: !lwjpb <jumper>");
+ RegConsoleCmd("sm_lajpb", CommandLAJPB, "[KZ] Show PB Ladder Jump in chat. Usage: !lajpb <jumper>");
+ RegConsoleCmd("sm_lahpb", CommandLAHPB, "[KZ] Show PB Ladderhop in chat. Usage: !lahpb <jumper>");
+ RegConsoleCmd("sm_jbpb", CommandJBPB, "[KZ] Show PB Jumpbug in chat. Usage: !jbpb <jumper>");
+ RegConsoleCmd("sm_js", CommandJS, "[KZ] Open a menu showing jumpstat PBs. Usage: !js <jumper>");
+ RegConsoleCmd("sm_jumpstats", CommandJS, "[KZ] Open a menu showing jumpstat PBs. Usage: !jumpstats <jumper>");
+ RegConsoleCmd("sm_jstop", CommandJSTop, "[KZ] Open a menu showing the top jumpstats.");
+ RegConsoleCmd("sm_jumptop", CommandJSTop, "[KZ] Open a menu showing the top jumpstats.");
+
+ RegAdminCmd("sm_updatemappool", CommandUpdateMapPool, ADMFLAG_ROOT, "[KZ] Update the ranked map pool with the list of maps in cfg/sourcemod/gokz/gokz-localranks-mappool.cfg.");
+}
+
+public Action CommandTop(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ DisplayPlayerTopModeMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandMapTop(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Open map top for current map
+ DB_OpenMapTopModeMenu(client, GOKZ_DB_GetCurrentMapID(), 0);
+ }
+ else if (args >= 1)
+ { // Open map top for specified map
+ char specifiedMap[33];
+ GetCmdArg(1, specifiedMap, sizeof(specifiedMap));
+ DB_OpenMapTopModeMenu_FindMap(client, specifiedMap, 0);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandBMapTop(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Open Bonus 1 top for current map
+ DB_OpenMapTopModeMenu(client, GOKZ_DB_GetCurrentMapID(), 1);
+ }
+ else if (args == 1)
+ { // Open specified Bonus # top for current map
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_OpenMapTopModeMenu(client, GOKZ_DB_GetCurrentMapID(), bonus);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args >= 2)
+ { // Open specified bonus top for specified map
+ char argBonus[4], argMap[33];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_OpenMapTopModeMenu_FindMap(client, argMap, bonus);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandPB(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Print their PBs for current map and their current mode
+ DB_PrintPBs(client, GetSteamAccountID(client), GOKZ_DB_GetCurrentMapID(), 0, GOKZ_GetCoreOption(client, Option_Mode));
+ if (gB_GOKZGlobal)
+ {
+ char steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ GOKZ_GL_PrintRecords(client, "", 0, GOKZ_GetCoreOption(client, Option_Mode), steamid);
+ }
+ }
+ else if (args == 1)
+ { // Print their PBs for specified map and their current mode
+ char argMap[33];
+ GetCmdArg(1, argMap, sizeof(argMap));
+ DB_PrintPBs_FindMap(client, GetSteamAccountID(client), argMap, 0, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else if (args >= 2)
+ { // Print specified player's PBs for specified map and their current mode
+ char argMap[33], argPlayer[MAX_NAME_LENGTH];
+ GetCmdArg(1, argMap, sizeof(argMap));
+ GetCmdArg(2, argPlayer, sizeof(argPlayer));
+ DB_PrintPBs_FindPlayerAndMap(client, argPlayer, argMap, 0, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandBPB(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Print their Bonus 1 PBs for current map and their current mode
+ DB_PrintPBs(client, GetSteamAccountID(client), GOKZ_DB_GetCurrentMapID(), 1, GOKZ_GetCoreOption(client, Option_Mode));
+ if (gB_GOKZGlobal)
+ {
+ char steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ GOKZ_GL_PrintRecords(client, "", 1, GOKZ_GetCoreOption(client, Option_Mode), steamid);
+ }
+ }
+ else if (args == 1)
+ { // Print their specified Bonus # PBs for current map and their current mode
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintPBs(client, GetSteamAccountID(client), GOKZ_DB_GetCurrentMapID(), bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ if (gB_GOKZGlobal)
+ {
+ char steamid[32];
+ GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));
+ GOKZ_GL_PrintRecords(client, "", bonus, GOKZ_GetCoreOption(client, Option_Mode), steamid);
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args == 2)
+ { // Print their specified Bonus # PBs for specified map and their current mode
+ char argBonus[4], argMap[33];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintPBs_FindMap(client, GetSteamAccountID(client), argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args >= 3)
+ { // Print specified player's specified Bonus # PBs for specified map and their current mode
+ char argBonus[4], argMap[33], argPlayer[MAX_NAME_LENGTH];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ GetCmdArg(3, argPlayer, sizeof(argPlayer));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintPBs_FindPlayerAndMap(client, argPlayer, argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandWR(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Print record times for current map and their current mode
+ DB_PrintRecords(client, GOKZ_DB_GetCurrentMapID(), 0, GOKZ_GetCoreOption(client, Option_Mode));
+ if (gB_GOKZGlobal)
+ {
+ GOKZ_GL_PrintRecords(client, "", 0, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ }
+ else if (args >= 1)
+ { // Print record times for specified map and their current mode
+ char argMap[33];
+ GetCmdArg(1, argMap, sizeof(argMap));
+ DB_PrintRecords_FindMap(client, argMap, 0, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandBWR(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Print Bonus 1 record times for current map and their current mode
+ DB_PrintRecords(client, GOKZ_DB_GetCurrentMapID(), 1, GOKZ_GetCoreOption(client, Option_Mode));
+ if (gB_GOKZGlobal)
+ {
+ GOKZ_GL_PrintRecords(client, "", 1, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ }
+ else if (args == 1)
+ { // Print specified Bonus # record times for current map and their current mode
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintRecords(client, GOKZ_DB_GetCurrentMapID(), bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ if (gB_GOKZGlobal)
+ {
+ GOKZ_GL_PrintRecords(client, "", bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args >= 2)
+ { // Print specified Bonus # record times for specified map and their current mode
+ char argBonus[4], argMap[33];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintRecords_FindMap(client, argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandAVG(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Print average times for current map and their current mode
+ DB_PrintAverage(client, GOKZ_DB_GetCurrentMapID(), 0, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else if (args >= 1)
+ { // Print average times for specified map and their current mode
+ char argMap[33];
+ GetCmdArg(1, argMap, sizeof(argMap));
+ DB_PrintAverage_FindMap(client, argMap, 0, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandBAVG(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ { // Print Bonus 1 average times for current map and their current mode
+ DB_PrintAverage(client, GOKZ_DB_GetCurrentMapID(), 1, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else if (args == 1)
+ { // Print specified Bonus # average times for current map and their current mode
+ char argBonus[4];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintAverage(client, GOKZ_DB_GetCurrentMapID(), bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ else if (args >= 2)
+ { // Print specified Bonus # average times for specified map and their current mode
+ char argBonus[4], argMap[33];
+ GetCmdArg(1, argBonus, sizeof(argBonus));
+ GetCmdArg(2, argMap, sizeof(argMap));
+ int bonus = StringToInt(argBonus);
+ if (GOKZ_IsValidCourse(bonus, true))
+ {
+ DB_PrintAverage_FindMap(client, argMap, bonus, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Invalid Bonus Number", argBonus);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandPC(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args < 1)
+ {
+ DB_GetCompletion(client, GetSteamAccountID(client), GOKZ_GetCoreOption(client, Option_Mode), true);
+ }
+ else if (args >= 1)
+ { // Print record times for specified map and their current mode
+ char argPlayer[MAX_NAME_LENGTH];
+ GetCmdArg(1, argPlayer, sizeof(argPlayer));
+ DB_GetCompletion_FindPlayer(client, argPlayer, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandRecentRecords(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ DisplayRecentRecordsModeMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandUpdateMapPool(int client, int args)
+{
+ DB_UpdateRankedMapPool(client);
+ return Plugin_Handled;
+}
+
+public Action CommandLJPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_LongJump);
+ return Plugin_Handled;
+}
+
+public Action CommandBHPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_Bhop);
+ return Plugin_Handled;
+}
+
+public Action CommandLBHPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_LowpreBhop);
+ return Plugin_Handled;
+}
+
+public Action CommandMBHPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_MultiBhop);
+ return Plugin_Handled;
+}
+
+public Action CommandWJPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_WeirdJump);
+ return Plugin_Handled;
+}
+
+public Action CommandLWJPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_LowpreWeirdJump);
+ return Plugin_Handled;
+}
+
+public Action CommandLAJPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_LadderJump);
+ return Plugin_Handled;
+}
+
+public Action CommandLAHPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_Ladderhop);
+ return Plugin_Handled;
+}
+
+public Action CommandJBPB(int client, int args)
+{
+ DisplayJumpstatRecordCommand(client, args, JumpType_Jumpbug);
+ return Plugin_Handled;
+}
+
+public Action CommandJS(int client, int args)
+{
+ if (IsSpammingCommands(client))
+ {
+ return Plugin_Handled;
+ }
+
+ if (args < 1)
+ {
+ DB_OpenJumpStatsModeMenu(client, GetSteamAccountID(client));
+ }
+ else if (args >= 1)
+ {
+ char argPlayer[MAX_NAME_LENGTH];
+ GetCmdArg(1, argPlayer, sizeof(argPlayer));
+ DB_OpenJumpStatsModeMenu_FindPlayer(client, argPlayer);
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandJSTop(int client, int args)
+{
+ DisplayJumpTopModeMenu(client);
+ return Plugin_Handled;
+}
+
+void DisplayJumpstatRecordCommand(int client, int args, int jumpType)
+{
+ if (args >= 1)
+ {
+ char argJumper[33];
+ GetCmdArg(1, argJumper, sizeof(argJumper));
+ DisplayJumpstatRecord(client, jumpType, argJumper);
+ }
+ else
+ {
+ DisplayJumpstatRecord(client, jumpType);
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+bool IsSpammingCommands(int client, bool printMessage = true)
+{
+ float currentTime = GetEngineTime();
+ float timeSinceLastCommand = currentTime - lastCommandTime[client];
+ if (timeSinceLastCommand < LR_COMMAND_COOLDOWN)
+ {
+ if (printMessage)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Please Wait Before Using Command", LR_COMMAND_COOLDOWN - timeSinceLastCommand + 0.1);
+ }
+ return true;
+ }
+
+ // Not spamming commands - all good!
+ lastCommandTime[client] = currentTime;
+ return false;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp b/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp
new file mode 100644
index 0000000..12c3ed2
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/cache_pbs.sp
@@ -0,0 +1,62 @@
+/*
+ Caches the player's personal best times on the map.
+*/
+
+
+
+void DB_CachePBs(int client, int steamID)
+{
+ char query[1024];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Reset PB exists array
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++)
+ {
+ gB_PBExistsCache[client][course][mode][timeType] = false;
+ }
+ }
+ }
+
+ int mapID = GOKZ_DB_GetCurrentMapID();
+
+ // Get Map PBs
+ FormatEx(query, sizeof(query), sql_getpbs, steamID, mapID);
+ txn.AddQuery(query);
+ // Get PRO PBs
+ FormatEx(query, sizeof(query), sql_getpbspro, steamID, mapID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_CachePBs, DB_TxnFailure_Generic, GetClientUserId(client), DBPrio_High);
+}
+
+public void DB_TxnSuccess_CachePBs(Handle db, int userID, int numQueries, Handle[] results, any[] queryData)
+{
+ int client = GetClientOfUserId(userID);
+ if (client < 1 || client > MaxClients || !IsClientAuthorized(client) || IsFakeClient(client))
+ {
+ return;
+ }
+
+ int course, mode;
+
+ while (SQL_FetchRow(results[0]))
+ {
+ course = SQL_FetchInt(results[0], 1);
+ mode = SQL_FetchInt(results[0], 2);
+ gB_PBExistsCache[client][course][mode][TimeType_Nub] = true;
+ gF_PBTimesCache[client][course][mode][TimeType_Nub] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[0], 0));
+ }
+
+ while (SQL_FetchRow(results[1]))
+ {
+ course = SQL_FetchInt(results[1], 1);
+ mode = SQL_FetchInt(results[1], 2);
+ gB_PBExistsCache[client][course][mode][TimeType_Pro] = true;
+ gF_PBTimesCache[client][course][mode][TimeType_Pro] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[1], 0));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/cache_records.sp b/sourcemod/scripting/gokz-localranks/db/cache_records.sp
new file mode 100644
index 0000000..611b13c
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/cache_records.sp
@@ -0,0 +1,54 @@
+/*
+ Caches the record times on the map.
+*/
+
+
+
+void DB_CacheRecords(int mapID)
+{
+ char query[1024];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Reset record exists array
+ for (int course = 0; course < GOKZ_MAX_COURSES; course++)
+ {
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++)
+ {
+ gB_RecordExistsCache[course][mode][timeType] = false;
+ }
+ }
+ }
+
+ // Get Map WRs
+ FormatEx(query, sizeof(query), sql_getwrs, mapID);
+ txn.AddQuery(query);
+ // Get PRO WRs
+ FormatEx(query, sizeof(query), sql_getwrspro, mapID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_CacheRecords, DB_TxnFailure_Generic, _, DBPrio_High);
+}
+
+public void DB_TxnSuccess_CacheRecords(Handle db, any data, int numQueries, Handle[] results, any[] queryData)
+{
+ int course, mode;
+
+ while (SQL_FetchRow(results[0]))
+ {
+ course = SQL_FetchInt(results[0], 1);
+ mode = SQL_FetchInt(results[0], 2);
+ gB_RecordExistsCache[course][mode][TimeType_Nub] = true;
+ gF_RecordTimesCache[course][mode][TimeType_Nub] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[0], 0));
+ }
+
+ while (SQL_FetchRow(results[1]))
+ {
+ course = SQL_FetchInt(results[1], 1);
+ mode = SQL_FetchInt(results[1], 2);
+ gB_RecordExistsCache[course][mode][TimeType_Pro] = true;
+ gF_RecordTimesCache[course][mode][TimeType_Pro] = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[1], 0));
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/create_tables.sp b/sourcemod/scripting/gokz-localranks/db/create_tables.sp
new file mode 100644
index 0000000..a15f67c
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/create_tables.sp
@@ -0,0 +1,27 @@
+/*
+ Table creation and alteration.
+*/
+
+
+
+void DB_CreateTables()
+{
+ Transaction txn = SQL_CreateTransaction();
+
+ // Create/alter database tables
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ txn.AddQuery(sqlite_maps_alter1);
+ }
+ case DatabaseType_MySQL:
+ {
+ txn.AddQuery(mysql_maps_alter1);
+ }
+ }
+
+ // No error logs for this transaction as it will always throw an error
+ // if the column already exists, which is more annoying than helpful.
+ SQL_ExecuteTransaction(gH_DB, txn, _, _, _, DBPrio_High);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/display_js.sp b/sourcemod/scripting/gokz-localranks/db/display_js.sp
new file mode 100644
index 0000000..779148f
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/display_js.sp
@@ -0,0 +1,325 @@
+/*
+ Displays player's best jumpstats in a menu.
+*/
+
+static int jumpStatsTargetSteamID[MAXPLAYERS + 1];
+static char jumpStatsTargetAlias[MAXPLAYERS + 1][MAX_NAME_LENGTH];
+static int jumpStatsMode[MAXPLAYERS + 1];
+
+
+
+// =====[ JUMPSTATS MODE ]=====
+
+void DB_OpenJumpStatsModeMenu(int client, int targetSteamID)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(targetSteamID);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Retrieve name of target
+ FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenJumpStatsModeMenu, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenJumpStatsModeMenu(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int targetSteamID = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ // Get name of target
+ if (!SQL_FetchRow(results[0]))
+ {
+ return;
+ }
+ SQL_FetchString(results[0], 0, jumpStatsTargetAlias[client], sizeof(jumpStatsTargetAlias[]));
+
+ jumpStatsTargetSteamID[client] = targetSteamID;
+ DisplayJumpStatsModeMenu(client);
+}
+
+void DB_OpenJumpStatsModeMenu_FindPlayer(int client, const char[] target)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteString(target);
+
+ DB_FindPlayer(target, DB_TxnSuccess_OpenJumpStatsModeMenu_FindPlayer, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenJumpStatsModeMenu_FindPlayer(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ char playerSearch[33];
+ data.ReadString(playerSearch, sizeof(playerSearch));
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ else if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Player Not Found", playerSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]))
+ {
+ DB_OpenJumpStatsModeMenu(client, SQL_FetchInt(results[0], 0));
+ }
+}
+
+
+
+// =====[ MENUS ]=====
+
+static void DisplayJumpStatsModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_JumpStatsMode);
+ menu.SetTitle("%T", "Jump Stats Mode Menu - Title", client, jumpStatsTargetAlias[client]);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void DisplayJumpStatsBlockTypeMenu(int client, int mode)
+{
+ jumpStatsMode[client] = mode;
+
+ Menu menu = new Menu(MenuHandler_JumpStatsBlockType);
+ menu.SetTitle("%T", "Jump Stats Block Type Menu - Title", client, jumpStatsTargetAlias[client], gC_ModeNames[jumpStatsMode[client]]);
+ JumpStatsBlockTypeMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void JumpStatsBlockTypeMenuAddItems(int client, Menu menu)
+{
+ char str[64];
+ FormatEx(str, sizeof(str), "%T", "Jump Records", client);
+ menu.AddItem("jump", str);
+ FormatEx(str, sizeof(str), "%T %T", "Block", client, "Jump Records", client);
+ menu.AddItem("blockjump", str);
+}
+
+
+
+// =====[ JUMPSTATS ]=====
+
+void DB_OpenJumpStats(int client, int targetSteamID, int mode, int blockType)
+{
+ char query[1024];
+ Transaction txn = SQL_CreateTransaction();
+
+ // Get alias
+ FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID);
+ txn.AddQuery(query);
+
+ // Get jumpstat pbs
+ if (blockType == 0)
+ {
+ FormatEx(query, sizeof(query), sql_jumpstats_getpbs, targetSteamID, mode);
+ }
+ else
+ {
+ FormatEx(query, sizeof(query), sql_jumpstats_getblockpbs, targetSteamID, mode);
+ }
+ txn.AddQuery(query);
+
+ DataPack datapack = new DataPack();
+ datapack.WriteCell(GetClientUserId(client));
+ datapack.WriteCell(targetSteamID);
+ datapack.WriteCell(mode);
+ datapack.WriteCell(blockType);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenJumpStats, DB_TxnFailure_Generic_DataPack, datapack, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenJumpStats(Handle db, DataPack datapack, int numQueries, Handle[] results, any[] queryData)
+{
+ datapack.Reset();
+ int client = GetClientOfUserId(datapack.ReadCell());
+ int targetSteamID = datapack.ReadCell();
+ int mode = datapack.ReadCell();
+ int blockType = datapack.ReadCell();
+ delete datapack;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ // Get target name
+ if (!SQL_FetchRow(results[0]))
+ {
+ return;
+ }
+ char alias[MAX_NAME_LENGTH];
+ SQL_FetchString(results[0], 0, alias, sizeof(alias));
+
+ if (SQL_GetRowCount(results[1]) == 0)
+ {
+ if (blockType == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%T", "Jump Stats Menu - No Jump Stats", client, alias);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%T", "Jump Stats Menu - No Block Jump Stats", client, alias);
+ }
+
+ DisplayJumpStatsBlockTypeMenu(client, mode);
+ return;
+ }
+
+ Menu menu = new Menu(MenuHandler_JumpStatsSubmenu);
+ if (blockType == 0)
+ {
+ menu.SetTitle("%T", "Jump Stats Submenu - Title (Jump)", client, alias, gC_ModeNames[mode]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Jump Stats Submenu - Title (Block Jump)", client, alias, gC_ModeNames[mode]);
+ }
+
+ char buffer[128], admin[64];
+ bool clientIsAdmin = CheckCommandAccess(client, "sm_deletejump", ADMFLAG_ROOT, false);
+
+ if (blockType == 0)
+ {
+ FormatEx(buffer, sizeof(buffer), "%T", "Jump Stats - Jump Console Header",
+ client, gC_ModeNames[mode], alias, targetSteamID & 1, targetSteamID >> 1);
+ PrintToConsole(client, "%s", buffer);
+ int titleLength = strlen(buffer);
+ strcopy(buffer, sizeof(buffer), "----------------------------------------------------------------");
+ buffer[titleLength] = '\0';
+ PrintToConsole(client, "%s", buffer);
+
+ while (SQL_FetchRow(results[1]))
+ {
+ int jumpid = SQL_FetchInt(results[1], JumpstatDB_PBMenu_JumpID);
+ int jumpType = SQL_FetchInt(results[1], JumpstatDB_PBMenu_JumpType);
+ float distance = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Distance) / GOKZ_DB_JS_DISTANCE_PRECISION;
+ int strafes = SQL_FetchInt(results[1], JumpstatDB_PBMenu_Strafes);
+ float sync = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Sync) / GOKZ_DB_JS_SYNC_PRECISION;
+ float pre = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Pre) / GOKZ_DB_JS_PRE_PRECISION;
+ float max = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Max) / GOKZ_DB_JS_MAX_PRECISION;
+ float airtime = SQL_FetchFloat(results[1], JumpstatDB_PBMenu_Air) / GOKZ_DB_JS_AIRTIME_PRECISION;
+
+ FormatEx(buffer, sizeof(buffer), "%0.4f %s", distance, gC_JumpTypes[jumpType]);
+ menu.AddItem("", buffer, ITEMDRAW_DISABLED);
+
+ FormatEx(buffer, sizeof(buffer), "%8s", gC_JumpTypesShort[jumpType]);
+ buffer[3] = '\0';
+
+ if (clientIsAdmin)
+ {
+ FormatEx(admin, sizeof(admin), "<id: %d>", jumpid);
+ }
+
+ PrintToConsole(client, "%s %0.4f [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s",
+ buffer, distance, strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air", admin);
+ }
+ }
+ else
+ {
+ FormatEx(buffer, sizeof(buffer), "%T", "Jump Stats - Block Jump Console Header",
+ client, gC_ModeNames[mode], alias, targetSteamID & 1, targetSteamID >> 1);
+ PrintToConsole(client, "%s", buffer);
+ int titleLength = strlen(buffer);
+ strcopy(buffer, sizeof(buffer), "----------------------------------------------------------------");
+ buffer[titleLength] = '\0';
+ PrintToConsole(client, "%s", buffer);
+
+ while (SQL_FetchRow(results[1]))
+ {
+ int jumpid = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_JumpID);
+ int jumpType = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_JumpType);
+ int block = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_Block);
+ float distance = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Distance) / GOKZ_DB_JS_DISTANCE_PRECISION;
+ int strafes = SQL_FetchInt(results[1], JumpstatDB_BlockPBMenu_Strafes);
+ float sync = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Sync) / GOKZ_DB_JS_SYNC_PRECISION;
+ float pre = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Pre) / GOKZ_DB_JS_PRE_PRECISION;
+ float max = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Max) / GOKZ_DB_JS_MAX_PRECISION;
+ float airtime = SQL_FetchFloat(results[1], JumpstatDB_BlockPBMenu_Air) / GOKZ_DB_JS_AIRTIME_PRECISION;
+
+ FormatEx(buffer, sizeof(buffer), "%d %T (%0.4f) %s", block, "Block", client, distance, gC_JumpTypes[jumpType]);
+ menu.AddItem("", buffer, ITEMDRAW_DISABLED);
+
+ FormatEx(buffer, sizeof(buffer), "%8s", gC_JumpTypesShort[jumpType]);
+ buffer[3] = '\0';
+
+ if (clientIsAdmin)
+ {
+ FormatEx(admin, sizeof(admin), "<id: %d>", jumpid);
+ }
+
+ PrintToConsole(client, "%s %d %t (%0.4f) [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s",
+ buffer, block, "Block", distance, strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air", admin);
+ }
+ }
+
+ PrintToConsole(client, "");
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ MENU HANDLERS ]=====
+
+public int MenuHandler_JumpStatsMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = mode
+ DisplayJumpStatsBlockTypeMenu(param1, param2);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_JumpStatsBlockType(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = blockType
+ DB_OpenJumpStats(param1, jumpStatsTargetSteamID[param1], jumpStatsMode[param1], param2);
+ }
+ else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayJumpStatsModeMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_JumpStatsSubmenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayJumpStatsBlockTypeMenu(param1, jumpStatsMode[param1]);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/get_completion.sp b/sourcemod/scripting/gokz-localranks/db/get_completion.sp
new file mode 100644
index 0000000..fbc76e7
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/get_completion.sp
@@ -0,0 +1,155 @@
+/*
+ Gets the number and percentage of maps completed.
+*/
+
+
+
+void DB_GetCompletion(int client, int targetSteamID, int mode, bool print)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(targetSteamID);
+ data.WriteCell(mode);
+ data.WriteCell(print);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Retrieve Alias of SteamID
+ FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID);
+ txn.AddQuery(query);
+ // Get total number of ranked main courses
+ txn.AddQuery(sql_getcount_maincourses);
+ // Get number of main course completions
+ FormatEx(query, sizeof(query), sql_getcount_maincoursescompleted, targetSteamID, mode);
+ txn.AddQuery(query);
+ // Get number of main course completions (PRO)
+ FormatEx(query, sizeof(query), sql_getcount_maincoursescompletedpro, targetSteamID, mode);
+ txn.AddQuery(query);
+
+ // Get total number of ranked bonuses
+ txn.AddQuery(sql_getcount_bonuses);
+ // Get number of bonus completions
+ FormatEx(query, sizeof(query), sql_getcount_bonusescompleted, targetSteamID, mode);
+ txn.AddQuery(query);
+ // Get number of bonus completions (PRO)
+ FormatEx(query, sizeof(query), sql_getcount_bonusescompletedpro, targetSteamID, mode);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_GetCompletion, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_GetCompletion(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int targetSteamID = data.ReadCell();
+ int mode = data.ReadCell();
+ bool print = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ char playerName[MAX_NAME_LENGTH];
+ int totalMainCourses, completions, completionsPro;
+ int totalBonuses, bonusCompletions, bonusCompletionsPro;
+
+ // Get Player Name from results
+ if (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, playerName, sizeof(playerName));
+ }
+
+ // Get total number of main courses
+ if (SQL_FetchRow(results[1]))
+ {
+ totalMainCourses = SQL_FetchInt(results[1], 0);
+ }
+ // Get completed main courses
+ if (SQL_FetchRow(results[2]))
+ {
+ completions = SQL_FetchInt(results[2], 0);
+ }
+ // Get completed main courses (PRO)
+ if (SQL_FetchRow(results[3]))
+ {
+ completionsPro = SQL_FetchInt(results[3], 0);
+ }
+
+ // Get total number of bonuses
+ if (SQL_FetchRow(results[4]))
+ {
+ totalBonuses = SQL_FetchInt(results[4], 0);
+ }
+ // Get completed bonuses
+ if (SQL_FetchRow(results[5])) {
+ bonusCompletions = SQL_FetchInt(results[5], 0);
+ }
+ // Get completed bonuses (PRO)
+ if (SQL_FetchRow(results[6]))
+ {
+ bonusCompletionsPro = SQL_FetchInt(results[6], 0);
+ }
+
+ // Print completion message to chat if specified
+ if (print)
+ {
+ if (totalMainCourses + totalBonuses == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Ranked Maps");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Completion",
+ playerName,
+ completions, totalMainCourses, completionsPro, totalMainCourses,
+ bonusCompletions, totalBonuses, bonusCompletionsPro, totalBonuses,
+ gC_ModeNamesShort[mode]);
+ }
+ }
+
+ // Set scoreboard MVP stars to percentage PRO completion of server's default mode
+ if (totalMainCourses + totalBonuses != 0 && targetSteamID == GetSteamAccountID(client) && mode == GOKZ_GetDefaultMode())
+ {
+ CS_SetMVPCount(client, RoundToFloor(float(completionsPro + bonusCompletionsPro) / float(totalMainCourses + totalBonuses) * 100.0));
+ }
+}
+
+void DB_GetCompletion_FindPlayer(int client, const char[] target, int mode)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteString(target);
+ data.WriteCell(mode);
+
+ DB_FindPlayer(target, DB_TxnSuccess_GetCompletion_FindPlayer, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_GetCompletion_FindPlayer(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ char playerSearch[33];
+ data.ReadString(playerSearch, sizeof(playerSearch));
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ else if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Player Not Found", playerSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]))
+ {
+ DB_GetCompletion(client, SQL_FetchInt(results[0], 0), mode, true);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/helpers.sp b/sourcemod/scripting/gokz-localranks/db/helpers.sp
new file mode 100644
index 0000000..670a420
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/helpers.sp
@@ -0,0 +1,91 @@
+/*
+ Database helper functions and callbacks.
+*/
+
+
+
+/* Error report callback for failed transactions */
+public void DB_TxnFailure_Generic(Handle db, any data, int numQueries, const char[] error, int failIndex, any[] queryData)
+{
+ LogError("Database transaction error: %s", error);
+}
+
+/* Error report callback for failed transactions which deletes the DataPack */
+public void DB_TxnFailure_Generic_DataPack(Handle db, DataPack data, int numQueries, const char[] error, int failIndex, any[] queryData)
+{
+ delete data;
+ LogError("Database transaction error: %s", error);
+}
+
+/* Used to search the database for a player name and return their PlayerID and alias
+
+ For SQLTxnSuccess onSuccess:
+ results[0] - 0:PlayerID, 1:Alias
+*/
+void DB_FindPlayer(const char[] playerSearch, SQLTxnSuccess onSuccess, any data = 0, DBPriority priority = DBPrio_Normal)
+{
+ char query[1024], playerEscaped[MAX_NAME_LENGTH * 2 + 1];
+ SQL_EscapeString(gH_DB, playerSearch, playerEscaped, sizeof(playerEscaped));
+
+ String_ToLower(playerEscaped, playerEscaped, sizeof(playerEscaped));
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Look for player name and retrieve their PlayerID
+ FormatEx(query, sizeof(query), sql_players_searchbyalias, playerEscaped, playerEscaped);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, onSuccess, DB_TxnFailure_Generic, data, priority);
+}
+
+/* Used to search the database for a map name and return its MapID and name
+
+ For SQLTxnSuccess onSuccess:
+ results[0] - 0:MapID, 1:Name
+*/
+void DB_FindMap(const char[] mapSearch, SQLTxnSuccess onSuccess, any data = 0, DBPriority priority = DBPrio_Normal)
+{
+ char query[1024], mapEscaped[129];
+ SQL_EscapeString(gH_DB, mapSearch, mapEscaped, sizeof(mapEscaped));
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Look for map name and retrieve it's MapID
+ FormatEx(query, sizeof(query), sql_maps_searchbyname, mapEscaped, mapEscaped);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, onSuccess, DB_TxnFailure_Generic, data, priority);
+}
+
+/* Used to search the database for a player name and return their PlayerID and alias,
+ and search the database for a map name and return its MapID and name
+
+ For SQLTxnSuccess onSuccess:
+ results[0] - 0:PlayerID, 1:Alias
+ results[1] - 0:MapID, 1:Name
+*/
+void DB_FindPlayerAndMap(const char[] playerSearch, const char[] mapSearch, SQLTxnSuccess onSuccess, any data = 0, DBPriority priority = DBPrio_Normal)
+{
+ char query[1024], mapEscaped[129], playerEscaped[MAX_NAME_LENGTH * 2 + 1];
+ SQL_EscapeString(gH_DB, playerSearch, playerEscaped, sizeof(playerEscaped));
+ SQL_EscapeString(gH_DB, mapSearch, mapEscaped, sizeof(mapEscaped));
+
+ String_ToLower(playerEscaped, playerEscaped, sizeof(playerEscaped));
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Look for player name and retrieve their PlayerID
+ FormatEx(query, sizeof(query), sql_players_searchbyalias, playerEscaped, playerEscaped);
+ txn.AddQuery(query);
+ // Look for map name and retrieve it's MapID
+ FormatEx(query, sizeof(query), sql_maps_searchbyname, mapEscaped, mapEscaped);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, onSuccess, DB_TxnFailure_Generic, data, priority);
+}
+
+// Used to convert the Account ID to the SteamID we can use for a Global API query
+int GetSteam2FromAccountId(char[] result, int maxlen, int account_id)
+{
+ return Format(result, maxlen, "STEAM_1:%d:%d", view_as<bool>(account_id % 2), account_id / 2);
+}
diff --git a/sourcemod/scripting/gokz-localranks/db/js_top.sp b/sourcemod/scripting/gokz-localranks/db/js_top.sp
new file mode 100644
index 0000000..a336a90
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/js_top.sp
@@ -0,0 +1,286 @@
+
+static int jumpTopMode[MAXPLAYERS + 1];
+static int jumpTopType[MAXPLAYERS + 1];
+static int blockNums[MAXPLAYERS + 1][JS_TOP_RECORD_COUNT];
+static int jumpInfo[MAXPLAYERS + 1][JS_TOP_RECORD_COUNT][3];
+
+
+
+void DB_OpenJumpTop(int client, int mode, int jumpType, int blockType)
+{
+ char query[1024];
+
+ Transaction txn = SQL_CreateTransaction();
+
+ FormatEx(query, sizeof(query), sql_jumpstats_gettop, jumpType, mode, blockType, jumpType, mode, blockType, JS_TOP_RECORD_COUNT);
+ txn.AddQuery(query);
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(mode);
+ data.WriteCell(jumpType);
+ data.WriteCell(blockType);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_GetJumpTop, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+void DB_TxnSuccess_GetJumpTop(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int mode = data.ReadCell();
+ int type = data.ReadCell();
+ int blockType = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ jumpTopMode[client] = mode;
+ jumpTopType[client] = type;
+
+ int rows = SQL_GetRowCount(results[0]);
+ if (rows == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Jumpstats Found");
+ DisplayJumpTopBlockTypeMenu(client, mode, type);
+ return;
+ }
+
+ char display[128], alias[33], title[65], admin[65];
+ int jumpid, steamid, block, strafes;
+ float distance, sync, pre, max, airtime;
+
+ bool clientIsAdmin = CheckCommandAccess(client, "sm_deletejump", ADMFLAG_ROOT, false);
+
+ Menu menu = new Menu(MenuHandler_JumpTopList);
+ menu.Pagination = 5;
+
+ if (blockType == 0)
+ {
+ menu.SetTitle("%T", "Jump Top Submenu - Title (Jump)", client, gC_ModeNames[mode], gC_JumpTypes[type]);
+
+ FormatEx(title, sizeof(title), "%s %s %T", gC_ModeNames[mode], gC_JumpTypes[type], "Top", client);
+ strcopy(display, sizeof(display), "----------------------------------------------------------------");
+ display[strlen(title)] = '\0';
+
+ PrintToConsole(client, title);
+ PrintToConsole(client, display);
+
+ for (int i = 0; i < rows; i++)
+ {
+ SQL_FetchRow(results[0]);
+ jumpid = SQL_FetchInt(results[0], JumpstatDB_Top20_JumpID);
+ steamid = SQL_FetchInt(results[0], JumpstatDB_Top20_SteamID);
+ SQL_FetchString(results[0], JumpstatDB_Top20_Alias, alias, sizeof(alias));
+ distance = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Distance)) / GOKZ_DB_JS_DISTANCE_PRECISION;
+ strafes = SQL_FetchInt(results[0], JumpstatDB_Top20_Strafes);
+ sync = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Sync)) / GOKZ_DB_JS_SYNC_PRECISION;
+ pre = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Pre)) / GOKZ_DB_JS_PRE_PRECISION;
+ max = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Max)) / GOKZ_DB_JS_MAX_PRECISION;
+ airtime = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Air)) / GOKZ_DB_JS_AIRTIME_PRECISION;
+
+ FormatEx(display, sizeof(display), "#%-2d %.4f %s", i + 1, distance, alias);
+
+ menu.AddItem(IntToStringEx(i), display);
+
+ if (clientIsAdmin)
+ {
+ FormatEx(admin, sizeof(admin), "<id: %d>", jumpid);
+ }
+
+ PrintToConsole(client, "#%-2d %.4f %s <STEAM_1:%d:%d> [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s",
+ i + 1, distance, alias, steamid & 1, steamid >> 1,
+ strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air",
+ admin);
+
+ jumpInfo[client][i][0] = steamid;
+ jumpInfo[client][i][1] = type;
+ jumpInfo[client][i][2] = mode;
+ blockNums[client][i] = 0;
+ }
+ }
+ else
+ {
+ menu.SetTitle("%T", "Jump Top Submenu - Title (Block Jump)", client, gC_ModeNames[mode], gC_JumpTypes[type]);
+
+ FormatEx(title, sizeof(title), "%s %T %s %T", gC_ModeNames[mode], "Block", client, gC_JumpTypes[type], "Top", client);
+ strcopy(display, sizeof(display), "----------------------------------------------------------------");
+ display[strlen(title)] = '\0';
+
+ PrintToConsole(client, title);
+ PrintToConsole(client, display);
+
+ for (int i = 0; i < rows; i++)
+ {
+ SQL_FetchRow(results[0]);
+ jumpid = SQL_FetchInt(results[0], JumpstatDB_Top20_JumpID);
+ steamid = SQL_FetchInt(results[0], JumpstatDB_Top20_SteamID);
+ SQL_FetchString(results[0], JumpstatDB_Top20_Alias, alias, sizeof(alias));
+ block = SQL_FetchInt(results[0], JumpstatDB_Top20_Block);
+ distance = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Distance)) / GOKZ_DB_JS_DISTANCE_PRECISION;
+ strafes = SQL_FetchInt(results[0], JumpstatDB_Top20_Strafes);
+ sync = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Sync)) / GOKZ_DB_JS_SYNC_PRECISION;
+ pre = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Pre)) / GOKZ_DB_JS_PRE_PRECISION;
+ max = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Max)) / GOKZ_DB_JS_MAX_PRECISION;
+ airtime = float(SQL_FetchInt(results[0], JumpstatDB_Top20_Air)) / GOKZ_DB_JS_AIRTIME_PRECISION;
+
+ FormatEx(display, sizeof(display), "#%-2d %d %T (%.4f) %s", i + 1, block, "Block", client, distance, alias);
+ menu.AddItem(IntToStringEx(i), display);
+
+ if (clientIsAdmin)
+ {
+ FormatEx(admin, sizeof(admin), "<id: %d>", jumpid);
+ }
+
+ PrintToConsole(client, "#%-2d %d %t (%.4f) %s <STEAM_1:%d:%d> [%d %t | %.2f%% %t | %.2f %t | %.2f %t | %.4f %t] %s",
+ i + 1, block, "Block", distance, alias, steamid & 1, steamid >> 1,
+ strafes, "Strafes", sync, "Sync", pre, "Pre", max, "Max", airtime, "Air",
+ admin);
+
+ jumpInfo[client][i][0] = steamid;
+ jumpInfo[client][i][1] = type;
+ jumpInfo[client][i][2] = mode;
+ blockNums[client][i] = block;
+ }
+ }
+ menu.Display(client, MENU_TIME_FOREVER);
+ PrintToConsole(client, "");
+}
+
+// =====[ MENUS ]=====
+
+void DisplayJumpTopModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_JumpTopMode);
+ menu.SetTitle("%T", "Jump Top Mode Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+void DisplayJumpTopTypeMenu(int client, int mode)
+{
+ jumpTopMode[client] = mode;
+
+ Menu menu = new Menu(MenuHandler_JumpTopType);
+ menu.SetTitle("%T", "Jump Top Type Menu - Title", client, gC_ModeNames[jumpTopMode[client]]);
+ JumpTopTypeMenuAddItems(menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void JumpTopTypeMenuAddItems(Menu menu)
+{
+ char display[32];
+ for (int i = 0; i < JUMPTYPE_COUNT - 3; i++)
+ {
+ FormatEx(display, sizeof(display), "%s", gC_JumpTypes[i]);
+ menu.AddItem(IntToStringEx(i), display);
+ }
+}
+
+void DisplayJumpTopBlockTypeMenu(int client, int mode, int type)
+{
+ jumpTopMode[client] = mode;
+ jumpTopType[client] = type;
+
+ Menu menu = new Menu(MenuHandler_JumpTopBlockType);
+ menu.SetTitle("%T", "Jump Top Block Type Menu - Title", client, gC_ModeNames[jumpTopMode[client]], gC_JumpTypes[jumpTopType[client]]);
+ JumpTopBlockTypeMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void JumpTopBlockTypeMenuAddItems(int client, Menu menu)
+{
+ char str[64];
+ FormatEx(str, sizeof(str), "%T", "Jump Records", client);
+ menu.AddItem("jump", str);
+ FormatEx(str, sizeof(str), "%T %T", "Block", client, "Jump Records", client);
+ menu.AddItem("blockjump", str);
+}
+
+
+
+// =====[ MENU HANDLERS ]=====
+
+public int MenuHandler_JumpTopMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = mode
+ DisplayJumpTopTypeMenu(param1, param2);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_JumpTopType(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = type
+ DisplayJumpTopBlockTypeMenu(param1, jumpTopMode[param1], param2);
+ }
+ else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayJumpTopModeMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_JumpTopBlockType(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = block type
+ DB_OpenJumpTop(param1, jumpTopMode[param1], jumpTopType[param1], param2);
+ }
+ else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayJumpTopTypeMenu(param1, jumpTopMode[param1]);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_JumpTopList(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char path[PLATFORM_MAX_PATH];
+ if (blockNums[param1][param2] == 0)
+ {
+ BuildPath(Path_SM, path, sizeof(path),
+ "%s/%d/%d_%s_%s.%s",
+ RP_DIRECTORY_JUMPS, jumpInfo[param1][param2][0], jumpTopType[param1], gC_ModeNamesShort[jumpInfo[param1][param2][2]], gC_StyleNamesShort[0], RP_FILE_EXTENSION);
+ }
+ else
+ {
+ BuildPath(Path_SM, path, sizeof(path),
+ "%s/%d/%s/%d_%d_%s_%s.%s",
+ RP_DIRECTORY_JUMPS, jumpInfo[param1][param2][0], RP_DIRECTORY_BLOCKJUMPS, jumpTopType[param1], blockNums[param1][param2], gC_ModeNamesShort[jumpInfo[param1][param2][2]], gC_StyleNamesShort[0], RP_FILE_EXTENSION);
+ }
+ GOKZ_RP_LoadJumpReplay(param1, path);
+ }
+
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayJumpTopBlockTypeMenu(param1, jumpTopMode[param1], jumpTopType[param1]);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-localranks/db/map_top.sp b/sourcemod/scripting/gokz-localranks/db/map_top.sp
new file mode 100644
index 0000000..5b296e9
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/map_top.sp
@@ -0,0 +1,388 @@
+/*
+ Opens a menu with the top times for a map course and mode.
+*/
+
+
+
+#define ITEM_INFO_GLOBAL_TOP_NUB "gn"
+#define ITEM_INFO_GLOBAL_TOP_PRO "gp"
+
+static char mapTopMap[MAXPLAYERS + 1][64];
+static int mapTopMapID[MAXPLAYERS + 1];
+static int mapTopCourse[MAXPLAYERS + 1];
+static int mapTopMode[MAXPLAYERS + 1];
+
+
+
+// =====[ MAP TOP MODE ]=====
+
+void DB_OpenMapTopModeMenu(int client, int mapID, int course)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(mapID);
+ data.WriteCell(course);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Retrieve Map Name of MapID
+ FormatEx(query, sizeof(query), sql_maps_getname, mapID);
+ txn.AddQuery(query);
+ // Check for existence of map course with that MapID and Course
+ FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenMapTopModeMenu, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenMapTopModeMenu(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int mapID = data.ReadCell();
+ int course = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ // Get name of map
+ if (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, mapTopMap[client], sizeof(mapTopMap[]));
+ }
+ // Check if the map course exists in the database
+ if (SQL_GetRowCount(results[1]) == 0)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapTopMap[client]);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapTopMap[client], course);
+ }
+ return;
+ }
+
+ mapTopMapID[client] = mapID;
+ mapTopCourse[client] = course;
+ DisplayMapTopModeMenu(client);
+}
+
+void DB_OpenMapTopModeMenu_FindMap(int client, const char[] mapSearch, int course)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteString(mapSearch);
+ data.WriteCell(course);
+
+ DB_FindMap(mapSearch, DB_TxnSuccess_OpenMapTopModeMenu_FindMap, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenMapTopModeMenu_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ char mapSearch[33];
+ data.ReadString(mapSearch, sizeof(mapSearch));
+ int course = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]))
+ { // Result is the MapID
+ DB_OpenMapTopModeMenu(client, SQL_FetchInt(results[0], 0), course);
+ }
+}
+
+
+
+// =====[ MAP TOP ]=====
+
+void DB_OpenMapTop(int client, int mapID, int course, int mode, int timeType)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(course);
+ data.WriteCell(mode);
+ data.WriteCell(timeType);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Get map name
+ FormatEx(query, sizeof(query), sql_maps_getname, mapID);
+ txn.AddQuery(query);
+ // Check for existence of map course with that MapID and Course
+ FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course);
+ txn.AddQuery(query);
+
+ // Get top times for each time type
+ switch (timeType)
+ {
+ case TimeType_Nub:FormatEx(query, sizeof(query), sql_getmaptop, mapID, course, mode, LR_MAP_TOP_CUTOFF);
+ case TimeType_Pro:FormatEx(query, sizeof(query), sql_getmaptoppro, mapID, course, mode, LR_MAP_TOP_CUTOFF);
+ }
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenMapTop, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenMapTop(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ int timeType = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ // Get map name from results
+ char mapName[64];
+ if (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, mapName, sizeof(mapName));
+ }
+ // Check if the map course exists in the database
+ if (SQL_GetRowCount(results[1]) == 0)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course);
+ }
+ return;
+ }
+
+ // Check if there are any times
+ if (SQL_GetRowCount(results[2]) == 0)
+ {
+ switch (timeType)
+ {
+ case TimeType_Nub:GOKZ_PrintToChat(client, true, "%t", "No Times Found");
+ case TimeType_Pro:GOKZ_PrintToChat(client, true, "%t", "No Times Found (PRO)");
+ }
+ DisplayMapTopMenu(client, mode);
+ return;
+ }
+
+ Menu menu = new Menu(MenuHandler_MapTopSubmenu);
+ menu.Pagination = 5;
+
+ // Set submenu title
+ if (course == 0)
+ {
+ menu.SetTitle("%T", "Map Top Submenu - Title", client,
+ LR_MAP_TOP_CUTOFF, gC_TimeTypeNames[timeType], mapName, gC_ModeNames[mode]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Map Top Submenu - Title (Bonus)", client,
+ LR_MAP_TOP_CUTOFF, gC_TimeTypeNames[timeType], mapName, course, gC_ModeNames[mode]);
+ }
+
+ // Add submenu items
+ char display[128], title[65], admin[65];
+ char playerName[MAX_NAME_LENGTH];
+ float runTime;
+ int timeid, steamid, teleports, rank = 0;
+
+ bool clientIsAdmin = CheckCommandAccess(client, "sm_deletetime", ADMFLAG_ROOT, false);
+
+ FormatEx(title, sizeof(title), "%s %s %s %T", gC_ModeNames[mode], mapName, gC_TimeTypeNames[timeType], "Top", client);
+ strcopy(display, sizeof(display), "----------------------------------------------------------------");
+ display[strlen(title)] = '\0';
+ PrintToConsole(client, title);
+ PrintToConsole(client, display);
+
+ while (SQL_FetchRow(results[2]))
+ {
+ rank++;
+ timeid = SQL_FetchInt(results[2], 0);
+ steamid = SQL_FetchInt(results[2], 1);
+ SQL_FetchString(results[2], 2, playerName, sizeof(playerName));
+ runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[2], 3));
+
+ if (clientIsAdmin)
+ {
+ FormatEx(admin, sizeof(admin), "<id: %d>", timeid);
+ }
+
+ switch (timeType)
+ {
+ case TimeType_Nub:
+ {
+ teleports = SQL_FetchInt(results[2], 4);
+ FormatEx(display, sizeof(display), "#%-2d %11s %3d TP %s",
+ rank, GOKZ_FormatTime(runTime), teleports, playerName);
+
+ PrintToConsole(client, "#%-2d %11s %3d TP %s <STEAM_1:%d:%d> %s",
+ rank, GOKZ_FormatTime(runTime), teleports, playerName, steamid & 1, steamid >> 1, admin);
+ }
+ case TimeType_Pro:
+ {
+ FormatEx(display, sizeof(display), "#%-2d %11s %s",
+ rank, GOKZ_FormatTime(runTime), playerName);
+
+ PrintToConsole(client, "#%-2d %11s %s <STEAM_1:%d:%d> %s",
+ rank, GOKZ_FormatTime(runTime), playerName, steamid & 1, steamid >> 1, admin);
+ }
+ }
+ menu.AddItem("", display, ITEMDRAW_DISABLED);
+ }
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ MENUS ]=====
+
+void DisplayMapTopModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_MapTopMode);
+ MapTopModeMenuSetTitle(client, menu);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void MapTopModeMenuSetTitle(int client, Menu menu)
+{
+ if (mapTopCourse[client] == 0)
+ {
+ menu.SetTitle("%T", "Map Top Mode Menu - Title", client, mapTopMap[client]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Map Top Mode Menu - Title (Bonus)", client, mapTopMap[client], mapTopCourse[client]);
+ }
+}
+
+void DisplayMapTopMenu(int client, int mode)
+{
+ mapTopMode[client] = mode;
+
+ Menu menu = new Menu(MenuHandler_MapTop);
+ if (mapTopCourse[client] == 0)
+ {
+ menu.SetTitle("%T", "Map Top Menu - Title", client,
+ mapTopMap[client], gC_ModeNames[mapTopMode[client]]);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Map Top Menu - Title (Bonus)", client,
+ mapTopMap[client], mapTopCourse[client], gC_ModeNames[mapTopMode[client]]);
+ }
+ MapTopMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void MapTopMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+ for (int i = 0; i < TIMETYPE_COUNT; i++)
+ {
+ FormatEx(display, sizeof(display), "%T", "Map Top Menu - Top", client, LR_MAP_TOP_CUTOFF, gC_TimeTypeNames[i]);
+ menu.AddItem(IntToStringEx(i), display);
+ }
+ if (gB_GOKZGlobal)
+ {
+ FormatEx(display, sizeof(display), "%T", "Map Top Menu - Global Top", client, gC_TimeTypeNames[TimeType_Nub]);
+ menu.AddItem(ITEM_INFO_GLOBAL_TOP_NUB, display);
+
+ FormatEx(display, sizeof(display), "%T", "Map Top Menu - Global Top", client, gC_TimeTypeNames[TimeType_Pro]);
+ menu.AddItem(ITEM_INFO_GLOBAL_TOP_PRO, display);
+ }
+}
+
+void ReopenMapTopMenu(int client)
+{
+ DisplayMapTopMenu(client, mapTopMode[client]);
+}
+
+
+
+// =====[ MENU HANDLERS ]=====
+
+public int MenuHandler_MapTopMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ // param1 = client, param2 = mode
+ DisplayMapTopMenu(param1, param2);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_MapTop(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[8];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (gB_GOKZGlobal && StrEqual(info, ITEM_INFO_GLOBAL_TOP_NUB))
+ {
+ GOKZ_GL_DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1], TimeType_Nub);
+ }
+ else if (gB_GOKZGlobal && StrEqual(info, ITEM_INFO_GLOBAL_TOP_PRO))
+ {
+ GOKZ_GL_DisplayMapTopMenu(param1, mapTopMap[param1], mapTopCourse[param1], mapTopMode[param1], TimeType_Pro);
+ }
+ else
+ {
+ int timeType = StringToInt(info);
+ DB_OpenMapTop(param1, mapTopMapID[param1], mapTopCourse[param1], mapTopMode[param1], timeType);
+ }
+ }
+ else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayMapTopModeMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_MapTopSubmenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ // TODO Menu item info is player's SteamID32, but is currently not used
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ ReopenMapTopMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/player_top.sp b/sourcemod/scripting/gokz-localranks/db/player_top.sp
new file mode 100644
index 0000000..0348eec
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/player_top.sp
@@ -0,0 +1,165 @@
+/*
+ Opens a menu with top record holders of a time type and mode.
+*/
+
+
+
+static int playerTopMode[MAXPLAYERS + 1];
+
+
+
+void DB_OpenPlayerTop(int client, int timeType, int mode)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(timeType);
+ data.WriteCell(mode);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Get top players
+ switch (timeType)
+ {
+ case TimeType_Nub:
+ {
+ FormatEx(query, sizeof(query), sql_gettopplayers, mode, LR_PLAYER_TOP_CUTOFF);
+ txn.AddQuery(query);
+ }
+ case TimeType_Pro:
+ {
+ FormatEx(query, sizeof(query), sql_gettopplayerspro, mode, LR_PLAYER_TOP_CUTOFF);
+ txn.AddQuery(query);
+ }
+ }
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenPlayerTop, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenPlayerTop(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int timeType = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ switch (timeType)
+ {
+ case TimeType_Nub:GOKZ_PrintToChat(client, true, "%t", "Player Top - No Times");
+ case TimeType_Pro:GOKZ_PrintToChat(client, true, "%t", "Player Top - No Times (PRO)");
+ }
+ DisplayPlayerTopMenu(client, playerTopMode[client]);
+ return;
+ }
+
+ Menu menu = new Menu(MenuHandler_PlayerTopSubmenu);
+ menu.Pagination = 5;
+
+ // Set submenu title
+ menu.SetTitle("%T", "Player Top Submenu - Title", client,
+ LR_PLAYER_TOP_CUTOFF, gC_TimeTypeNames[timeType], gC_ModeNames[mode]);
+
+ // Add submenu items
+ char display[256];
+ int rank = 0;
+ while (SQL_FetchRow(results[0]))
+ {
+ rank++;
+ char playerString[33];
+ SQL_FetchString(results[0], 1, playerString, sizeof(playerString));
+ FormatEx(display, sizeof(display), "#%-2d %s (%d)", rank, playerString, SQL_FetchInt(results[0], 2));
+ menu.AddItem(IntToStringEx(SQL_FetchInt(results[0], 0)), display, ITEMDRAW_DISABLED);
+ }
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ MENUS ]=====
+
+void DisplayPlayerTopModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_PlayerTopMode);
+ menu.SetTitle("%T", "Player Top Mode Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+void DisplayPlayerTopMenu(int client, int mode)
+{
+ playerTopMode[client] = mode;
+
+ Menu menu = new Menu(MenuHandler_PlayerTop);
+ menu.SetTitle("%T", "Player Top Menu - Title", client, gC_ModeNames[mode]);
+ PlayerTopMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void PlayerTopMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+ for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++)
+ {
+ FormatEx(display, sizeof(display), "%T", "Player Top Menu - Top", client,
+ LR_PLAYER_TOP_CUTOFF, gC_TimeTypeNames[timeType]);
+ menu.AddItem("", display, ITEMDRAW_DEFAULT);
+ }
+}
+
+
+
+// =====[ MENU HANLDERS ]=====
+
+public int MenuHandler_PlayerTopMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ DisplayPlayerTopMenu(param1, param2);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_PlayerTop(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ DB_OpenPlayerTop(param1, param2, playerTopMode[param1]);
+ }
+ else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayPlayerTopModeMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_PlayerTopSubmenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ // Menu item info is player's SteamID32, but is currently not used
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayPlayerTopMenu(param1, playerTopMode[param1]);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/print_average.sp b/sourcemod/scripting/gokz-localranks/db/print_average.sp
new file mode 100644
index 0000000..7f7c4e4
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/print_average.sp
@@ -0,0 +1,152 @@
+/*
+ Gets the average personal best time of a course.
+*/
+
+
+
+void DB_PrintAverage(int client, int mapID, int course, int mode)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Retrieve Map Name of MapID
+ FormatEx(query, sizeof(query), sql_maps_getname, mapID);
+ txn.AddQuery(query);
+ // Check for existence of map course with that MapID and Course
+ FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course);
+ txn.AddQuery(query);
+ // Get Average PB Time
+ FormatEx(query, sizeof(query), sql_getaverage, mapID, course, mode);
+ txn.AddQuery(query);
+ // Get Average PRO PB Time
+ FormatEx(query, sizeof(query), sql_getaverage_pro, mapID, course, mode);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_PrintAverage, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintAverage(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ char mapName[33];
+ int mapCompletions, mapCompletionsPro;
+ float averageTime, averageTimePro;
+
+ // Get Map Name from results
+ if (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, mapName, sizeof(mapName));
+ }
+ if (SQL_GetRowCount(results[1]) == 0)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course);
+ }
+ return;
+ }
+
+ // Get number of completions and average time
+ if (SQL_FetchRow(results[2]))
+ {
+ mapCompletions = SQL_FetchInt(results[2], 1);
+ if (mapCompletions > 0)
+ {
+ averageTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[2], 0));
+ }
+ }
+
+ // Get number of completions and average time (PRO)
+ if (SQL_FetchRow(results[3]))
+ {
+ mapCompletionsPro = SQL_FetchInt(results[3], 1);
+ if (mapCompletions > 0)
+ {
+ averageTimePro = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[3], 0));
+ }
+ }
+
+ // Print average time header to chat
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Average Time Header", mapName, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Average Time Header (Bonus)", mapName, course, gC_ModeNamesShort[mode]);
+ }
+
+ if (mapCompletions == 0)
+ {
+ CPrintToChat(client, "%t", "No Times Found");
+ }
+ else if (mapCompletionsPro == 0)
+ {
+ CPrintToChat(client, "%t, %t",
+ "Average Time - NUB", GOKZ_FormatTime(averageTime), mapCompletions,
+ "Average Time - No PRO Time");
+ }
+ else
+ {
+ CPrintToChat(client, "%t, %t",
+ "Average Time - NUB", GOKZ_FormatTime(averageTime), mapCompletions,
+ "Average Time - PRO", GOKZ_FormatTime(averageTimePro), mapCompletionsPro);
+ }
+}
+
+void DB_PrintAverage_FindMap(int client, const char[] mapSearch, int course, int mode)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteString(mapSearch);
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ DB_FindMap(mapSearch, DB_TxnSuccess_PrintAverage_FindMap, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintAverage_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ char mapSearch[33];
+ data.ReadString(mapSearch, sizeof(mapSearch));
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]))
+ { // Result is the MapID
+ DB_PrintAverage(client, SQL_FetchInt(results[0], 0), course, mode);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/print_js.sp b/sourcemod/scripting/gokz-localranks/db/print_js.sp
new file mode 100644
index 0000000..e694a95
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/print_js.sp
@@ -0,0 +1,108 @@
+/*
+ Prints the player's personal best jumps.
+*/
+
+
+
+void DisplayJumpstatRecord(int client, int jumpType, char[] jumper = "")
+{
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+
+ int steamid;
+ char alias[33];
+ if (StrEqual(jumper, ""))
+ {
+ steamid = GetSteamAccountID(client);
+ FormatEx(alias, sizeof(alias), "%N", client);
+
+ DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 0);
+ DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 1);
+ }
+ else
+ {
+ DataPack data = new DataPack();
+ data.WriteCell(client);
+ data.WriteCell(jumpType);
+ data.WriteCell(mode);
+ data.WriteString(jumper);
+
+ DB_FindPlayer(jumper, DB_JS_TxnSuccess_LookupPlayer, data);
+ }
+}
+
+public void DB_JS_TxnSuccess_LookupPlayer(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ char jumper[MAX_NAME_LENGTH];
+
+ data.Reset();
+ int client = data.ReadCell();
+ int jumpType = data.ReadCell();
+ int mode = data.ReadCell();
+ data.ReadString(jumper, sizeof(jumper));
+ delete data;
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Player Not Found", jumper);
+ return;
+ }
+
+ char alias[33];
+ SQL_FetchRow(results[0]);
+ int steamid = SQL_FetchInt(results[0], JumpstatDB_FindPlayer_SteamID32);
+ SQL_FetchString(results[0], JumpstatDB_FindPlayer_Alias, alias, sizeof(alias));
+
+ DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 0);
+ DB_JS_OpenPlayerRecord(client, steamid, alias, jumpType, mode, 1);
+}
+
+void DB_JS_OpenPlayerRecord(int client, int steamid, char[] alias, int jumpType, int mode, int block)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(client);
+ data.WriteString(alias);
+ data.WriteCell(jumpType);
+ data.WriteCell(mode);
+
+ Transaction txn = SQL_CreateTransaction();
+ FormatEx(query, sizeof(query), sql_jumpstats_getrecord, steamid, jumpType, mode, block);
+ txn.AddQuery(query);
+ SQL_ExecuteTransaction(gH_DB, txn, DB_JS_TxnSuccess_OpenPlayerRecord, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_JS_TxnSuccess_OpenPlayerRecord(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ char alias[33];
+ data.Reset();
+ int client = data.ReadCell();
+ data.ReadString(alias, sizeof(alias));
+ int jumpType = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Jumpstats Found");
+ return;
+ }
+
+ SQL_FetchRow(results[0]);
+ float distance = float(SQL_FetchInt(results[0], JumpstatDB_Lookup_Distance)) / 10000;
+ int block = SQL_FetchInt(results[0], JumpstatDB_Lookup_Block);
+
+ if (block == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Jump Record", gC_ModeNamesShort[mode], gC_JumpTypes[jumpType], alias, distance);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Block Jump Record", gC_ModeNamesShort[mode], gC_JumpTypes[jumpType], alias, block, distance);
+ }
+}
diff --git a/sourcemod/scripting/gokz-localranks/db/print_pbs.sp b/sourcemod/scripting/gokz-localranks/db/print_pbs.sp
new file mode 100644
index 0000000..0d541d4
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/print_pbs.sp
@@ -0,0 +1,266 @@
+/*
+ Prints the player's personal times on a map course and given mode.
+*/
+
+
+
+void DB_PrintPBs(int client, int targetSteamID, int mapID, int course, int mode)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Retrieve Alias of SteamID
+ FormatEx(query, sizeof(query), sql_players_getalias, targetSteamID);
+ txn.AddQuery(query);
+ // Retrieve Map Name of MapID
+ FormatEx(query, sizeof(query), sql_maps_getname, mapID);
+ txn.AddQuery(query);
+ // Check for existence of map course with that MapID and Course
+ FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course);
+ txn.AddQuery(query);
+
+ // Get PB
+ FormatEx(query, sizeof(query), sql_getpb, targetSteamID, mapID, course, mode, 1);
+ txn.AddQuery(query);
+ // Get Rank
+ FormatEx(query, sizeof(query), sql_getmaprank, mapID, course, mode, targetSteamID, mapID, course, mode);
+ txn.AddQuery(query);
+ // Get Number of Players with Times
+ FormatEx(query, sizeof(query), sql_getlowestmaprank, mapID, course, mode);
+ txn.AddQuery(query);
+
+ // Get PRO PB
+ FormatEx(query, sizeof(query), sql_getpbpro, targetSteamID, mapID, course, mode, 1);
+ txn.AddQuery(query);
+ // Get PRO Rank
+ FormatEx(query, sizeof(query), sql_getmaprankpro, mapID, course, mode, targetSteamID, mapID, course, mode);
+ txn.AddQuery(query);
+ // Get Number of Players with PRO Times
+ FormatEx(query, sizeof(query), sql_getlowestmaprankpro, mapID, course, mode);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_PrintPBs, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintPBs(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ char playerName[MAX_NAME_LENGTH], mapName[33];
+
+ bool hasPB = false;
+ bool hasPBPro = false;
+
+ float runTime;
+ int teleportsUsed;
+ int rank;
+ int maxRank;
+
+ float runTimePro;
+ int rankPro;
+ int maxRankPro;
+
+ // Get Player Name from results
+ if (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, playerName, sizeof(playerName));
+ }
+ // Get Map Name from results
+ if (SQL_FetchRow(results[1]))
+ {
+ SQL_FetchString(results[1], 0, mapName, sizeof(mapName));
+ }
+ if (SQL_GetRowCount(results[2]) == 0)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course);
+ }
+ return;
+ }
+
+ // Get PB info from results
+ if (SQL_GetRowCount(results[3]) > 0)
+ {
+ hasPB = true;
+ if (SQL_FetchRow(results[3]))
+ {
+ runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[3], 0));
+ teleportsUsed = SQL_FetchInt(results[3], 1);
+ }
+ if (SQL_FetchRow(results[4]))
+ {
+ rank = SQL_FetchInt(results[4], 0);
+ }
+ if (SQL_FetchRow(results[5]))
+ {
+ maxRank = SQL_FetchInt(results[5], 0);
+ }
+ }
+ // Get PB info (Pro) from results
+ if (SQL_GetRowCount(results[6]) > 0)
+ {
+ hasPBPro = true;
+ if (SQL_FetchRow(results[6]))
+ {
+ runTimePro = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[6], 0));
+ }
+ if (SQL_FetchRow(results[7]))
+ {
+ rankPro = SQL_FetchInt(results[7], 0);
+ }
+ if (SQL_FetchRow(results[8]))
+ {
+ maxRankPro = SQL_FetchInt(results[8], 0);
+ }
+ }
+
+ // Print PB header to chat
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "PB Header", playerName, mapName, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "PB Header (Bonus)", playerName, mapName, course, gC_ModeNamesShort[mode]);
+ }
+
+ // Print PB times to chat
+ if (!hasPB)
+ {
+ CPrintToChat(client, "%t", "PB Time - No Times");
+ }
+ else if (!hasPBPro)
+ {
+ CPrintToChat(client, "%t", "PB Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, rank, maxRank);
+ CPrintToChat(client, "%t", "PB Time - No PRO Time");
+ }
+ else if (teleportsUsed == 0)
+ { // Their MAP PB has 0 teleports, and is therefore also their PRO PB
+ CPrintToChat(client, "%t", "PB Time - NUB and PRO", GOKZ_FormatTime(runTime), rank, maxRank, rankPro, maxRankPro);
+ }
+ else
+ {
+ CPrintToChat(client, "%t", "PB Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, rank, maxRank);
+ CPrintToChat(client, "%t", "PB Time - PRO", GOKZ_FormatTime(runTimePro), rankPro, maxRankPro);
+ }
+}
+
+void DB_PrintPBs_FindMap(int client, int targetSteamID, const char[] mapSearch, int course, int mode)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(targetSteamID);
+ data.WriteString(mapSearch);
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ DB_FindMap(mapSearch, DB_TxnSuccess_PrintPBs_FindMap, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintPBs_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int targetSteamID = data.ReadCell();
+ char mapSearch[33];
+ data.ReadString(mapSearch, sizeof(mapSearch));
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ // Check if the map course exists in the database
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]))
+ { // Result is the MapID
+ DB_PrintPBs(client, targetSteamID, SQL_FetchInt(results[0], 0), course, mode);
+ if (gB_GOKZGlobal)
+ {
+ char map[33], steamid[32];
+ SQL_FetchString(results[0], 1, map, sizeof(map));
+ GetSteam2FromAccountId(steamid, sizeof(steamid), targetSteamID);
+ GOKZ_GL_PrintRecords(client, map, course, GOKZ_GetCoreOption(client, Option_Mode), steamid);
+ }
+ }
+}
+
+void DB_PrintPBs_FindPlayerAndMap(int client, const char[] playerSearch, const char[] mapSearch, int course, int mode)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteString(playerSearch);
+ data.WriteString(mapSearch);
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ DB_FindPlayerAndMap(playerSearch, mapSearch, DB_TxnSuccess_PrintPBs_FindPlayerAndMap, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintPBs_FindPlayerAndMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ char playerSearch[MAX_NAME_LENGTH];
+ data.ReadString(playerSearch, sizeof(playerSearch));
+ char mapSearch[33];
+ data.ReadString(mapSearch, sizeof(mapSearch));
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Player Not Found", playerSearch);
+ return;
+ }
+ else if (SQL_GetRowCount(results[1]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]) && SQL_FetchRow(results[1]))
+ {
+ int accountid = SQL_FetchInt(results[0], 0);
+ DB_PrintPBs(client, accountid, SQL_FetchInt(results[1], 0), course, mode);
+ if (gB_GOKZGlobal)
+ {
+ char map[33], steamid[32];
+ SQL_FetchString(results[1], 1, map, sizeof(map));
+ GetSteam2FromAccountId(steamid, sizeof(steamid), accountid);
+ GOKZ_GL_PrintRecords(client, map, course, GOKZ_GetCoreOption(client, Option_Mode), steamid);
+ }
+ }
+}
diff --git a/sourcemod/scripting/gokz-localranks/db/print_records.sp b/sourcemod/scripting/gokz-localranks/db/print_records.sp
new file mode 100644
index 0000000..b5de03b
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/print_records.sp
@@ -0,0 +1,173 @@
+/*
+ Prints the record times on a map course and given mode.
+*/
+
+
+
+void DB_PrintRecords(int client, int mapID, int course, int mode)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Retrieve Map Name of MapID
+ FormatEx(query, sizeof(query), sql_maps_getname, mapID);
+ txn.AddQuery(query);
+ // Check for existence of map course with that MapID and Course
+ FormatEx(query, sizeof(query), sql_mapcourses_findid, mapID, course);
+ txn.AddQuery(query);
+
+ // Get Map WR
+ FormatEx(query, sizeof(query), sql_getmaptop, mapID, course, mode, 1);
+ txn.AddQuery(query);
+ // Get PRO WR
+ FormatEx(query, sizeof(query), sql_getmaptoppro, mapID, course, mode, 1);
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_PrintRecords, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintRecords(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ char mapName[33];
+
+ bool mapHasRecord = false;
+ bool mapHasRecordPro = false;
+
+ char recordHolder[33];
+ float runTime;
+ int teleportsUsed;
+
+ char recordHolderPro[33];
+ float runTimePro;
+
+ // Get Map Name from results
+ if (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, mapName, sizeof(mapName));
+ }
+ // Check if the map course exists in the database
+ if (SQL_GetRowCount(results[1]) == 0)
+ {
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Main Course Not Found", mapName);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Bonus Not Found", mapName, course);
+ }
+ return;
+ }
+
+ // Get WR info from results
+ if (SQL_GetRowCount(results[2]) > 0)
+ {
+ mapHasRecord = true;
+ if (SQL_FetchRow(results[2]))
+ {
+ SQL_FetchString(results[2], 2, recordHolder, sizeof(recordHolder));
+ runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[2], 3));
+ teleportsUsed = SQL_FetchInt(results[2], 4);
+ }
+ }
+ // Get Pro WR info from results
+ if (SQL_GetRowCount(results[3]) > 0)
+ {
+ mapHasRecordPro = true;
+ if (SQL_FetchRow(results[3]))
+ {
+ SQL_FetchString(results[3], 2, recordHolderPro, sizeof(recordHolderPro));
+ runTimePro = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[3], 3));
+ }
+ }
+
+ // Print WR header to chat
+ if (course == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "WR Header", mapName, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "WR Header (Bonus)", mapName, course, gC_ModeNamesShort[mode]);
+ }
+
+ // Print WR times to chat
+ if (!mapHasRecord)
+ {
+ CPrintToChat(client, "%t", "No Times Found");
+ }
+ else if (!mapHasRecordPro)
+ {
+ CPrintToChat(client, "%t", "WR Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, recordHolder);
+ CPrintToChat(client, "%t", "WR Time - No PRO Time");
+ }
+ else if (teleportsUsed == 0)
+ {
+ CPrintToChat(client, "%t", "WR Time - NUB and PRO", GOKZ_FormatTime(runTimePro), recordHolderPro);
+ }
+ else
+ {
+ CPrintToChat(client, "%t", "WR Time - NUB", GOKZ_FormatTime(runTime), teleportsUsed, recordHolder);
+ CPrintToChat(client, "%t", "WR Time - PRO", GOKZ_FormatTime(runTimePro), recordHolderPro);
+ }
+}
+
+void DB_PrintRecords_FindMap(int client, const char[] mapSearch, int course, int mode)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteString(mapSearch);
+ data.WriteCell(course);
+ data.WriteCell(mode);
+
+ DB_FindMap(mapSearch, DB_TxnSuccess_PrintRecords_FindMap, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_PrintRecords_FindMap(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ char mapSearch[33];
+ data.ReadString(mapSearch, sizeof(mapSearch));
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Map Not Found", mapSearch);
+ return;
+ }
+ else if (SQL_FetchRow(results[0]))
+ { // Result is the MapID
+ DB_PrintRecords(client, SQL_FetchInt(results[0], 0), course, mode);
+ if (gB_GOKZGlobal)
+ {
+ char map[33];
+ SQL_FetchString(results[0], 1, map, sizeof(map));
+ GOKZ_GL_PrintRecords(client, map, course, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/process_new_time.sp b/sourcemod/scripting/gokz-localranks/db/process_new_time.sp
new file mode 100644
index 0000000..f9aaf73
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/process_new_time.sp
@@ -0,0 +1,157 @@
+/*
+ Processes a newly submitted time, determining if the player beat their
+ personal best and if they beat the map course and mode's record time.
+*/
+
+
+
+void DB_ProcessNewTime(int client, int steamID, int mapID, int course, int mode, int style, int runTimeMS, int teleportsUsed)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(steamID);
+ data.WriteCell(mapID);
+ data.WriteCell(course);
+ data.WriteCell(mode);
+ data.WriteCell(style);
+ data.WriteCell(runTimeMS);
+ data.WriteCell(teleportsUsed);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ // Get Top 2 PBs
+ FormatEx(query, sizeof(query), sql_getpb, steamID, mapID, course, mode, 2);
+ txn.AddQuery(query);
+ // Get Rank
+ FormatEx(query, sizeof(query), sql_getmaprank, mapID, course, mode, steamID, mapID, course, mode);
+ txn.AddQuery(query);
+ // Get Number of Players with Times
+ FormatEx(query, sizeof(query), sql_getlowestmaprank, mapID, course, mode);
+ txn.AddQuery(query);
+
+ if (teleportsUsed == 0)
+ {
+ // Get Top 2 PRO PBs
+ FormatEx(query, sizeof(query), sql_getpbpro, steamID, mapID, course, mode, 2);
+ txn.AddQuery(query);
+ // Get PRO Rank
+ FormatEx(query, sizeof(query), sql_getmaprankpro, mapID, course, mode, steamID, mapID, course, mode);
+ txn.AddQuery(query);
+ // Get Number of Players with PRO Times
+ FormatEx(query, sizeof(query), sql_getlowestmaprankpro, mapID, course, mode);
+ txn.AddQuery(query);
+ }
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_ProcessTimerEnd, DB_TxnFailure_Generic_DataPack, data, DBPrio_Normal);
+}
+
+public void DB_TxnSuccess_ProcessTimerEnd(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int steamID = data.ReadCell();
+ int mapID = data.ReadCell();
+ int course = data.ReadCell();
+ int mode = data.ReadCell();
+ int style = data.ReadCell();
+ int runTimeMS = data.ReadCell();
+ int teleportsUsed = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ bool firstTime = SQL_GetRowCount(results[0]) == 1;
+ int pbDiff = 0;
+ int rank = -1;
+ int maxRank = -1;
+ if (!firstTime)
+ {
+ SQL_FetchRow(results[0]);
+ int pb = SQL_FetchInt(results[0], 0);
+ if (runTimeMS == pb) // New time is new PB
+ {
+ SQL_FetchRow(results[0]);
+ int oldPB = SQL_FetchInt(results[0], 0);
+ pbDiff = runTimeMS - oldPB;
+ }
+ else // Didn't beat PB
+ {
+ pbDiff = runTimeMS - pb;
+ }
+ }
+ // Get NUB Rank
+ SQL_FetchRow(results[1]);
+ rank = SQL_FetchInt(results[1], 0);
+ SQL_FetchRow(results[2]);
+ maxRank = SQL_FetchInt(results[2], 0);
+
+ // Repeat for PRO Runs
+ bool firstTimePro = false;
+ int pbDiffPro = 0;
+ int rankPro = -1;
+ int maxRankPro = -1;
+ if (teleportsUsed == 0)
+ {
+ firstTimePro = SQL_GetRowCount(results[3]) == 1;
+ if (!firstTimePro)
+ {
+ SQL_FetchRow(results[3]);
+ int pb = SQL_FetchInt(results[3], 0);
+ if (runTimeMS == pb) // New time is new PB
+ {
+ SQL_FetchRow(results[3]);
+ int oldPB = SQL_FetchInt(results[3], 0);
+ pbDiffPro = runTimeMS - oldPB;
+ }
+ else // Didn't beat PB
+ {
+ pbDiffPro = runTimeMS - pb;
+ }
+ }
+ // Get PRO Rank
+ SQL_FetchRow(results[4]);
+ rankPro = SQL_FetchInt(results[4], 0);
+ SQL_FetchRow(results[5]);
+ maxRankPro = SQL_FetchInt(results[5], 0);
+ }
+
+ // Call OnTimeProcessed forward
+ Call_OnTimeProcessed(
+ client,
+ steamID,
+ mapID,
+ course,
+ mode,
+ style,
+ GOKZ_DB_TimeIntToFloat(runTimeMS),
+ teleportsUsed,
+ firstTime,
+ GOKZ_DB_TimeIntToFloat(pbDiff),
+ rank,
+ maxRank,
+ firstTimePro,
+ GOKZ_DB_TimeIntToFloat(pbDiffPro),
+ rankPro,
+ maxRankPro);
+
+ // Call OnNewRecord forward
+ bool newWR = (firstTime || pbDiff < 0) && rank == 1;
+ bool newWRPro = (firstTimePro || pbDiffPro < 0) && rankPro == 1;
+ if (newWR && newWRPro)
+ {
+ Call_OnNewRecord(client, steamID, mapID, course, mode, style, RecordType_NubAndPro, GOKZ_DB_TimeIntToFloat(pbDiffPro), teleportsUsed);
+ }
+ else if (newWR)
+ {
+ Call_OnNewRecord(client, steamID, mapID, course, mode, style, RecordType_Nub, GOKZ_DB_TimeIntToFloat(pbDiff), teleportsUsed);
+ }
+ else if (newWRPro)
+ {
+ Call_OnNewRecord(client, steamID, mapID, course, mode, style, RecordType_Pro, GOKZ_DB_TimeIntToFloat(pbDiffPro), teleportsUsed);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-localranks/db/recent_records.sp b/sourcemod/scripting/gokz-localranks/db/recent_records.sp
new file mode 100644
index 0000000..939b132
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/recent_records.sp
@@ -0,0 +1,171 @@
+/*
+ Opens the menu with a list of recently broken records for the given mode
+ and time type.
+*/
+
+
+
+static int recentRecordsMode[MAXPLAYERS + 1];
+
+
+
+void DB_OpenRecentRecords(int client, int mode, int timeType)
+{
+ char query[1024];
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(mode);
+ data.WriteCell(timeType);
+
+ Transaction txn = SQL_CreateTransaction();
+
+ switch (timeType)
+ {
+ case TimeType_Nub:FormatEx(query, sizeof(query), sql_getrecentrecords, mode, LR_PLAYER_TOP_CUTOFF);
+ case TimeType_Pro:FormatEx(query, sizeof(query), sql_getrecentrecords_pro, mode, LR_PLAYER_TOP_CUTOFF);
+ }
+ txn.AddQuery(query);
+
+ SQL_ExecuteTransaction(gH_DB, txn, DB_TxnSuccess_OpenRecentRecords, DB_TxnFailure_Generic_DataPack, data, DBPrio_Low);
+}
+
+public void DB_TxnSuccess_OpenRecentRecords(Handle db, DataPack data, int numQueries, Handle[] results, any[] queryData)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int mode = data.ReadCell();
+ int timeType = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ // Check if there are any times
+ if (SQL_GetRowCount(results[0]) == 0)
+ {
+ switch (timeType)
+ {
+ case TimeType_Nub:GOKZ_PrintToChat(client, true, "%t", "No Times Found");
+ case TimeType_Pro:GOKZ_PrintToChat(client, true, "%t", "No Times Found (PRO)");
+ }
+ return;
+ }
+
+ Menu menu = new Menu(MenuHandler_RecentRecordsSubmenu);
+ menu.Pagination = 5;
+
+ // Set submenu title
+ menu.SetTitle("%T", "Recent Records Submenu - Title", client,
+ gC_TimeTypeNames[timeType], gC_ModeNames[mode]);
+
+ // Add submenu items
+ char display[256], mapName[64], playerName[33];
+ int course;
+ float runTime;
+
+ while (SQL_FetchRow(results[0]))
+ {
+ SQL_FetchString(results[0], 0, mapName, sizeof(mapName));
+ course = SQL_FetchInt(results[0], 1);
+ SQL_FetchString(results[0], 3, playerName, sizeof(playerName));
+ runTime = GOKZ_DB_TimeIntToFloat(SQL_FetchInt(results[0], 4));
+
+ if (course == 0)
+ {
+ FormatEx(display, sizeof(display), "%s - %s (%s)",
+ mapName, playerName, GOKZ_FormatTime(runTime));
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%s B%d - %s (%s)",
+ mapName, course, playerName, GOKZ_FormatTime(runTime));
+ }
+
+ menu.AddItem(IntToStringEx(SQL_FetchInt(results[0], 2)), display, ITEMDRAW_DISABLED);
+ }
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ MENUS ]=====
+
+void DisplayRecentRecordsModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_RecentRecordsMode);
+ menu.SetTitle("%T", "Recent Records Mode Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+void DisplayRecentRecordsTimeTypeMenu(int client, int mode)
+{
+ recentRecordsMode[client] = mode;
+
+ Menu menu = new Menu(MenuHandler_RecentRecordsTimeType);
+ menu.SetTitle("%T", "Recent Records Menu - Title", client, gC_ModeNames[recentRecordsMode[client]]);
+ RecentRecordsTimeTypeAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void RecentRecordsTimeTypeAddItems(int client, Menu menu)
+{
+ char display[32];
+ for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++)
+ {
+ FormatEx(display, sizeof(display), "%T", "Recent Records Menu - Record Type", client, gC_TimeTypeNames[timeType]);
+ menu.AddItem("", display, ITEMDRAW_DEFAULT);
+ }
+}
+
+
+
+// =====[ MENU HANDLERS ]=====
+
+public int MenuHandler_RecentRecordsMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ DisplayRecentRecordsTimeTypeMenu(param1, param2);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_RecentRecordsTimeType(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ DB_OpenRecentRecords(param1, recentRecordsMode[param1], param2);
+ }
+ else if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayRecentRecordsModeMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_RecentRecordsSubmenu(Menu menu, MenuAction action, int param1, int param2)
+{
+ // TODO Menu item info is course's MapCourseID, but is currently not used
+ if (action == MenuAction_Cancel && param2 == MenuCancel_Exit)
+ {
+ DisplayRecentRecordsTimeTypeMenu(param1, recentRecordsMode[param1]);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-localranks/db/sql.sp b/sourcemod/scripting/gokz-localranks/db/sql.sp
new file mode 100644
index 0000000..f768e9a
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/sql.sp
@@ -0,0 +1,411 @@
+/*
+ SQL query templates.
+*/
+
+
+
+// =====[ MAPS ]=====
+
+char sqlite_maps_alter1[] = "\
+ALTER TABLE Maps \
+ ADD InRankedPool INTEGER NOT NULL DEFAULT '0'";
+
+char mysql_maps_alter1[] = "\
+ALTER TABLE Maps \
+ ADD InRankedPool TINYINT NOT NULL DEFAULT '0'";
+
+char sqlite_maps_insertranked[] = "\
+INSERT OR IGNORE INTO Maps \
+ (InRankedPool, Name) \
+ VALUES (%d, '%s')";
+
+char sqlite_maps_updateranked[] = "\
+UPDATE OR IGNORE Maps \
+ SET InRankedPool=%d \
+ WHERE Name = '%s'";
+
+char mysql_maps_upsertranked[] = "\
+INSERT INTO Maps (InRankedPool, Name) \
+ VALUES (%d, '%s') \
+ ON DUPLICATE KEY UPDATE \
+ InRankedPool=VALUES(InRankedPool)";
+
+char sql_maps_reset_mappool[] = "\
+UPDATE Maps \
+ SET InRankedPool=0";
+
+char sql_maps_getname[] = "\
+SELECT Name \
+ FROM Maps \
+ WHERE MapID=%d";
+
+char sql_maps_searchbyname[] = "\
+SELECT MapID, Name \
+ FROM Maps \
+ WHERE Name LIKE '%%%s%%' \
+ ORDER BY (Name='%s') DESC, LENGTH(Name) \
+ LIMIT 1";
+
+
+
+// =====[ PLAYERS ]=====
+
+char sql_players_getalias[] = "\
+SELECT Alias \
+ FROM Players \
+ WHERE SteamID32=%d";
+
+char sql_players_searchbyalias[] = "\
+SELECT SteamID32, Alias \
+ FROM Players \
+ WHERE Players.Cheater=0 AND LOWER(Alias) LIKE '%%%s%%' \
+ ORDER BY (LOWER(Alias)='%s') DESC, LastPlayed DESC \
+ LIMIT 1";
+
+
+
+// =====[ MAPCOURSES ]=====
+
+char sql_mapcourses_findid[] = "\
+SELECT MapCourseID \
+ FROM MapCourses \
+ WHERE MapID=%d AND Course=%d";
+
+
+
+// =====[ GENERAL ]=====
+
+char sql_getpb[] = "\
+SELECT Times.RunTime, Times.Teleports \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ WHERE Times.SteamID32=%d AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d \
+ ORDER BY Times.RunTime \
+ LIMIT %d";
+
+char sql_getpbpro[] = "\
+SELECT Times.RunTime \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ WHERE Times.SteamID32=%d AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0 \
+ ORDER BY Times.RunTime \
+ LIMIT %d";
+
+char sql_getmaptop[] = "\
+SELECT t.TimeID, t.SteamID32, p.Alias, t.RunTime AS PBTime, t.Teleports \
+ FROM Times t \
+ INNER JOIN MapCourses mc ON mc.MapCourseID=t.MapCourseID \
+ INNER JOIN Players p ON p.SteamID32=t.SteamID32 \
+ LEFT OUTER JOIN Times t2 ON t2.SteamID32=t.SteamID32 \
+ AND t2.MapCourseID=t.MapCourseID AND t2.Mode=t.Mode AND t2.RunTime<t.RunTime \
+ WHERE t2.TimeID IS NULL AND p.Cheater=0 AND mc.MapID=%d AND mc.Course=%d AND t.Mode=%d \
+ ORDER BY PBTime \
+ LIMIT %d";
+
+char sql_getmaptoppro[] = "\
+SELECT t.TimeID, t.SteamID32, p.Alias, t.RunTime AS PBTime, t.Teleports \
+ FROM Times t \
+ INNER JOIN MapCourses mc ON mc.MapCourseID=t.MapCourseID \
+ INNER JOIN Players p ON p.SteamID32=t.SteamID32 \
+ LEFT OUTER JOIN Times t2 ON t2.SteamID32=t.SteamID32 AND t2.MapCourseID=t.MapCourseID \
+ AND t2.Mode=t.Mode AND t2.RunTime<t.RunTime AND t.Teleports=0 AND t2.Teleports=0 \
+ WHERE t2.TimeID IS NULL AND p.Cheater=0 AND mc.MapID=%d \
+ AND mc.Course=%d AND t.Mode=%d AND t.Teleports=0 \
+ ORDER BY PBTime \
+ LIMIT %d";
+
+char sql_getwrs[] = "\
+SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d \
+ GROUP BY MapCourses.Course, Times.Mode";
+
+char sql_getwrspro[] = "\
+SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d AND Times.Teleports=0 \
+ GROUP BY MapCourses.Course, Times.Mode";
+
+char sql_getpbs[] = "\
+SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ WHERE Times.SteamID32=%d AND MapCourses.MapID=%d \
+ GROUP BY MapCourses.Course, Times.Mode";
+
+char sql_getpbspro[] = "\
+SELECT MIN(Times.RunTime), MapCourses.Course, Times.Mode \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ WHERE Times.SteamID32=%d AND MapCourses.MapID=%d AND Times.Teleports=0 \
+ GROUP BY MapCourses.Course, Times.Mode";
+
+char sql_getmaprank[] = "\
+SELECT COUNT(DISTINCT Times.SteamID32) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d AND MapCourses.Course=%d \
+ AND Times.Mode=%d AND Times.RunTime < \
+ (SELECT MIN(Times.RunTime) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND Times.SteamID32=%d AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d) \
+ + 1";
+
+char sql_getmaprankpro[] = "\
+SELECT COUNT(DISTINCT Times.SteamID32) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d AND MapCourses.Course=%d \
+ AND Times.Mode=%d AND Times.Teleports=0 \
+ AND Times.RunTime < \
+ (SELECT MIN(Times.RunTime) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND Times.SteamID32=%d AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0) \
+ + 1";
+
+char sql_getlowestmaprank[] = "\
+SELECT COUNT(DISTINCT Times.SteamID32) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d";
+
+char sql_getlowestmaprankpro[] = "\
+SELECT COUNT(DISTINCT Times.SteamID32) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0";
+
+char sql_getcount_maincourses[] = "\
+SELECT COUNT(*) \
+ FROM MapCourses \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ WHERE Maps.InRankedPool=1 AND MapCourses.Course=0";
+
+char sql_getcount_maincoursescompleted[] = "\
+SELECT COUNT(DISTINCT Times.MapCourseID) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ WHERE Maps.InRankedPool=1 AND MapCourses.Course=0 \
+ AND Times.SteamID32=%d AND Times.Mode=%d";
+
+char sql_getcount_maincoursescompletedpro[] = "\
+SELECT COUNT(DISTINCT Times.MapCourseID) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ WHERE Maps.InRankedPool=1 AND MapCourses.Course=0 \
+ AND Times.SteamID32=%d AND Times.Mode=%d AND Times.Teleports=0";
+
+char sql_getcount_bonuses[] = "\
+SELECT COUNT(*) \
+ FROM MapCourses \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ WHERE Maps.InRankedPool=1 AND MapCourses.Course>0";
+
+char sql_getcount_bonusescompleted[] = "\
+SELECT COUNT(DISTINCT Times.MapCourseID) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ WHERE Maps.InRankedPool=1 AND MapCourses.Course>0 \
+ AND Times.SteamID32=%d AND Times.Mode=%d";
+
+char sql_getcount_bonusescompletedpro[] = "\
+SELECT COUNT(DISTINCT Times.MapCourseID) \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ WHERE Maps.InRankedPool=1 AND MapCourses.Course>0 \
+ AND Times.SteamID32=%d AND Times.Mode=%d AND Times.Teleports=0";
+
+char sql_gettopplayers[] = "\
+SELECT Players.SteamID32, Players.Alias, COUNT(*) AS RecordCount \
+ FROM Times \
+ INNER JOIN \
+ (SELECT Times.MapCourseID, Times.Mode, MIN(Times.RunTime) AS RecordTime \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND Maps.InRankedPool=1 AND MapCourses.Course=0 \
+ AND Times.Mode=%d \
+ GROUP BY Times.MapCourseID) Records \
+ ON Times.MapCourseID=Records.MapCourseID AND Times.Mode=Records.Mode AND Times.RunTime=Records.RecordTime \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ GROUP BY Players.SteamID32, Players.Alias \
+ ORDER BY RecordCount DESC \
+ LIMIT %d"; // Doesn't include bonuses
+
+char sql_gettopplayerspro[] = "\
+SELECT Players.SteamID32, Players.Alias, COUNT(*) AS RecordCount \
+ FROM Times \
+ INNER JOIN \
+ (SELECT Times.MapCourseID, Times.Mode, MIN(Times.RunTime) AS RecordTime \
+ FROM Times \
+ INNER JOIN MapCourses ON MapCourses.MapCourseID=Times.MapCourseID \
+ INNER JOIN Maps ON Maps.MapID=MapCourses.MapID \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ WHERE Players.Cheater=0 AND Maps.InRankedPool=1 AND MapCourses.Course=0 \
+ AND Times.Mode=%d AND Times.Teleports=0 \
+ GROUP BY Times.MapCourseID) Records \
+ ON Times.MapCourseID=Records.MapCourseID AND Times.Mode=Records.Mode AND Times.RunTime=Records.RecordTime AND Times.Teleports=0 \
+ INNER JOIN Players ON Players.SteamID32=Times.SteamID32 \
+ GROUP BY Players.SteamID32, Players.Alias \
+ ORDER BY RecordCount DESC \
+ LIMIT %d"; // Doesn't include bonuses
+
+char sql_getaverage[] = "\
+SELECT AVG(PBTime), COUNT(*) \
+ FROM \
+ (SELECT MIN(Times.RunTime) AS PBTime \
+ FROM Times \
+ INNER JOIN MapCourses ON Times.MapCourseID=MapCourses.MapCourseID \
+ INNER JOIN Players ON Times.SteamID32=Players.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d \
+ GROUP BY Times.SteamID32) AS PBTimes";
+
+char sql_getaverage_pro[] = "\
+SELECT AVG(PBTime), COUNT(*) \
+ FROM \
+ (SELECT MIN(Times.RunTime) AS PBTime \
+ FROM Times \
+ INNER JOIN MapCourses ON Times.MapCourseID=MapCourses.MapCourseID \
+ INNER JOIN Players ON Times.SteamID32=Players.SteamID32 \
+ WHERE Players.Cheater=0 AND MapCourses.MapID=%d \
+ AND MapCourses.Course=%d AND Times.Mode=%d AND Times.Teleports=0 \
+ GROUP BY Times.SteamID32) AS PBTimes";
+
+char sql_getrecentrecords[] = "\
+SELECT Maps.Name, MapCourses.Course, MapCourses.MapCourseID, Players.Alias, a.RunTime \
+ FROM Times AS a \
+ INNER JOIN MapCourses ON a.MapCourseID=MapCourses.MapCourseID \
+ INNER JOIN Maps ON MapCourses.MapID=Maps.MapID \
+ INNER JOIN Players ON a.SteamID32=Players.SteamID32 \
+ WHERE Players.Cheater=0 AND Maps.InRankedPool AND a.Mode=%d \
+ AND NOT EXISTS \
+ (SELECT * \
+ FROM Times AS b \
+ WHERE a.MapCourseID=b.MapCourseID AND a.Mode=b.Mode \
+ AND a.Created>b.Created AND a.RunTime>b.RunTime) \
+ ORDER BY a.TimeID DESC \
+ LIMIT %d";
+
+char sql_getrecentrecords_pro[] = "\
+SELECT Maps.Name, MapCourses.Course, MapCourses.MapCourseID, Players.Alias, a.RunTime \
+ FROM Times AS a \
+ INNER JOIN MapCourses ON a.MapCourseID=MapCourses.MapCourseID \
+ INNER JOIN Maps ON MapCourses.MapID=Maps.MapID \
+ INNER JOIN Players ON a.SteamID32=Players.SteamID32 \
+ WHERE Players.Cheater=0 AND Maps.InRankedPool AND a.Mode=%d AND a.Teleports=0 \
+ AND NOT EXISTS \
+ (SELECT * \
+ FROM Times AS b \
+ WHERE b.Teleports=0 AND a.MapCourseID=b.MapCourseID AND a.Mode=b.Mode \
+ AND a.Created>b.Created AND a.RunTime>b.RunTime) \
+ ORDER BY a.TimeID DESC \
+ LIMIT %d";
+
+
+
+// =====[ JUMPSTATS ]=====
+
+char sql_jumpstats_gettop[] = "\
+SELECT j.JumpID, p.SteamID32, p.Alias, j.Block, j.Distance, j.Strafes, j.Sync, j.Pre, j.Max, j.Airtime \
+ FROM \
+ Jumpstats j \
+ INNER JOIN \
+ Players p ON \
+ p.SteamID32=j.SteamID32 AND \
+ p.Cheater = 0 \
+ INNER JOIN \
+ ( \
+ SELECT j.SteamID32, j.JumpType, j.Mode, j.IsBlockJump, MAX(j.Distance) BestDistance \
+ FROM \
+ Jumpstats j \
+ INNER JOIN \
+ ( \
+ SELECT SteamID32, MAX(Block) AS MaxBlockDist \
+ FROM \
+ Jumpstats \
+ WHERE \
+ JumpType = %d AND \
+ Mode = %d AND \
+ IsBlockJump = %d \
+ GROUP BY SteamID32 \
+ ) MaxBlock ON \
+ j.SteamID32 = MaxBlock.SteamID32 AND \
+ j.Block = MaxBlock.MaxBlockDist \
+ WHERE \
+ j.JumpType = %d AND \
+ j.Mode = %d AND \
+ j.IsBlockJump = %d \
+ GROUP BY j.SteamID32, j.JumpType, j.Mode, j.IsBlockJump \
+ ) MaxDist ON \
+ j.SteamID32 = MaxDist.SteamID32 AND \
+ j.JumpType = MaxDist.JumpType AND \
+ j.Mode = MaxDist.Mode AND \
+ j.IsBlockJump = MaxDist.IsBlockJump AND \
+ j.Distance = MaxDist.BestDistance \
+ ORDER BY j.Block DESC, j.Distance DESC \
+ LIMIT %d";
+
+char sql_jumpstats_getrecord[] = "\
+SELECT JumpID, Distance, Block \
+ FROM \
+ Jumpstats rec \
+ WHERE \
+ SteamID32 = %d AND \
+ JumpType = %d AND \
+ Mode = %d AND \
+ IsBlockJump = %d \
+ ORDER BY Block DESC, Distance DESC";
+
+char sql_jumpstats_getpbs[] = "\
+SELECT b.JumpID, b.JumpType, b.Distance, b.Strafes, b.Sync, b.Pre, b.Max, b.Airtime \
+ FROM Jumpstats b \
+ INNER JOIN ( \
+ SELECT a.SteamID32, a.Mode, a.JumpType, MAX(a.Distance) Distance \
+ FROM Jumpstats a \
+ WHERE a.SteamID32=%d AND a.Mode=%d AND NOT a.IsBlockJump \
+ GROUP BY a.JumpType, a.Mode, a.SteamID32 \
+ ) a ON a.JumpType=b.JumpType AND a.Distance=b.Distance \
+ WHERE a.SteamID32=b.SteamID32 AND a.Mode=b.Mode AND NOT b.IsBlockJump \
+ ORDER BY b.JumpType";
+
+char sql_jumpstats_getblockpbs[] = "\
+SELECT c.JumpID, c.JumpType, c.Block, c.Distance, c.Strafes, c.Sync, c.Pre, c.Max, c.Airtime \
+ FROM Jumpstats c \
+ INNER JOIN ( \
+ SELECT a.SteamID32, a.Mode, a.JumpType, a.Block, MAX(b.Distance) Distance \
+ FROM Jumpstats b \
+ INNER JOIN ( \
+ SELECT a.SteamID32, a.Mode, a.JumpType, MAX(a.Block) Block \
+ FROM Jumpstats a \
+ WHERE a.SteamID32=%d AND a.Mode=%d AND a.IsBlockJump \
+ GROUP BY a.JumpType, a.Mode, a.SteamID32 \
+ ) a ON a.JumpType=b.JumpType AND a.Block=b.Block \
+ WHERE a.SteamID32=b.SteamID32 AND a.Mode=b.Mode AND b.IsBlockJump \
+ GROUP BY a.JumpType, a.Mode, a.SteamID32, a.Block \
+ ) b ON b.JumpType=c.JumpType AND b.Block=c.Block AND b.Distance=c.Distance \
+ WHERE b.SteamID32=c.SteamID32 AND b.Mode=c.Mode AND c.IsBlockJump \
+ ORDER BY c.JumpType";
diff --git a/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp b/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp
new file mode 100644
index 0000000..ee9bd6d
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/db/update_ranked_map_pool.sp
@@ -0,0 +1,104 @@
+/*
+ Inserts a list of maps read from a file into the Maps table,
+ and updates them to be part of the ranked map pool.
+*/
+
+
+
+void DB_UpdateRankedMapPool(int client)
+{
+ File file = OpenFile(LR_CFG_MAP_POOL, "r");
+ if (file == null)
+ {
+ LogError("Failed to load file: '%s'.", LR_CFG_MAP_POOL);
+ if (IsValidClient(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Ranked Map Pool - Error");
+ }
+ return;
+ }
+
+ char map[256];
+ int mapsCount = 0;
+
+ Transaction txn = new Transaction();
+
+ // Reset all maps to be unranked
+ txn.AddQuery(sql_maps_reset_mappool);
+
+ // Insert/Update maps in gokz-localranks-mappool.cfg to be ranked
+ while (file.ReadLine(map, sizeof(map)))
+ {
+ TrimString(map);
+ String_ToLower(map, map, sizeof(map));
+
+ // Ignore blank lines and comments
+ if (map[0] == '\0' || map[0] == ';' || (map[0] == '/' && map[1] == '/'))
+ {
+ continue;
+ }
+
+ mapsCount++;
+
+ switch (g_DBType)
+ {
+ case DatabaseType_SQLite:
+ {
+ char updateQuery[512];
+ gH_DB.Format(updateQuery, sizeof(updateQuery), sqlite_maps_updateranked, 1, map);
+
+ char insertQuery[512];
+ gH_DB.Format(insertQuery, sizeof(insertQuery), sqlite_maps_insertranked, 1, map);
+
+ txn.AddQuery(updateQuery);
+ txn.AddQuery(insertQuery);
+ }
+ case DatabaseType_MySQL:
+ {
+ char query[512];
+ gH_DB.Format(query, sizeof(query), mysql_maps_upsertranked, 1, map);
+
+ txn.AddQuery(query);
+ }
+ }
+ }
+
+ delete file;
+
+ if (mapsCount == 0)
+ {
+ LogError("No maps found in file: '%s'.", LR_CFG_MAP_POOL);
+
+ if (IsValidClient(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Ranked Map Pool - No Maps In File");
+ GOKZ_PlayErrorSound(client);
+ }
+
+ delete txn;
+ return;
+ }
+
+ // Pass client user ID (or -1) as data
+ int data = -1;
+ if (IsValidClient(client))
+ {
+ data = GetClientUserId(client);
+ }
+
+ gH_DB.Execute(txn, DB_TxnSuccess_UpdateRankedMapPool, DB_TxnFailure_Generic, data);
+}
+
+public void DB_TxnSuccess_UpdateRankedMapPool(Handle db, int userid, int numQueries, Handle[] results, any[] queryData)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client))
+ {
+ LogMessage("The ranked map pool was updated by %L.", client);
+ GOKZ_PrintToChat(client, true, "%t", "Ranked Map Pool - Success");
+ }
+ else
+ {
+ LogMessage("The ranked map pool was updated.");
+ }
+}
diff --git a/sourcemod/scripting/gokz-localranks/misc.sp b/sourcemod/scripting/gokz-localranks/misc.sp
new file mode 100644
index 0000000..0c6d96c
--- /dev/null
+++ b/sourcemod/scripting/gokz-localranks/misc.sp
@@ -0,0 +1,319 @@
+/*
+ Miscellaneous functions.
+*/
+
+
+
+// =====[ COMPLETION MVP STARS ]=====
+
+void CompletionMVPStarsUpdate(int client)
+{
+ DB_GetCompletion(client, GetSteamAccountID(client), GOKZ_GetDefaultMode(), false);
+}
+
+void CompletionMVPStarsUpdateAll()
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && !IsFakeClient(client))
+ {
+ CompletionMVPStarsUpdate(client);
+ }
+ }
+}
+
+
+
+// =====[ ANNOUNCEMENTS ]=====
+
+void PrecacheAnnouncementSounds()
+{
+ if (!LoadSounds())
+ {
+ SetFailState("Failed to load file: \"%s\".", LR_CFG_SOUNDS);
+ }
+}
+
+static bool LoadSounds()
+{
+ KeyValues kv = new KeyValues("sounds");
+ if (!kv.ImportFromFile(LR_CFG_SOUNDS))
+ {
+ return false;
+ }
+
+ char downloadPath[256];
+
+ kv.GetString("beatrecord", gC_BeatRecordSound, sizeof(gC_BeatRecordSound));
+ FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", gC_BeatRecordSound);
+ AddFileToDownloadsTable(downloadPath);
+ PrecacheSound(gC_BeatRecordSound, true);
+
+ delete kv;
+ return true;
+}
+
+static void PlayBeatRecordSound()
+{
+ GOKZ_EmitSoundToAll(gC_BeatRecordSound, _, "Server Record");
+}
+
+void AnnounceNewTime(
+ int client,
+ int course,
+ int mode,
+ float runTime,
+ int teleportsUsed,
+ bool firstTime,
+ float pbDiff,
+ int rank,
+ int maxRank,
+ bool firstTimePro,
+ float pbDiffPro,
+ int rankPro,
+ int maxRankPro)
+{
+ // Main Course
+ if (course == 0)
+ {
+ // Main Course PRO Times
+ if (teleportsUsed == 0)
+ {
+ if (firstTimePro)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Time - First Time (PRO)",
+ client, GOKZ_FormatTime(runTime), rankPro, maxRankPro, gC_ModeNamesShort[mode]);
+ }
+ else if (pbDiffPro < 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Time - Beat PB (PRO)",
+ client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiffPro)), rankPro, maxRankPro, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Time - Miss PB (PRO)",
+ client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiffPro), rankPro, maxRankPro, gC_ModeNamesShort[mode]);
+ }
+ }
+ // Main Course NUB Times
+ else
+ {
+ if (firstTime)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Time - First Time",
+ client, GOKZ_FormatTime(runTime), rank, maxRank, gC_ModeNamesShort[mode]);
+ }
+ else if (pbDiff < 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Time - Beat PB",
+ client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiff)), rank, maxRank, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Time - Miss PB",
+ client, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiff), rank, maxRank, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+ // Bonus Course
+ else
+ {
+ // Bonus Course PRO Times
+ if (teleportsUsed == 0)
+ {
+ if (firstTimePro)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - First Time (PRO)",
+ client, course, GOKZ_FormatTime(runTime), rankPro, maxRankPro, gC_ModeNamesShort[mode]);
+ }
+ else if (pbDiffPro < 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Beat PB (PRO)",
+ client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiffPro)), rankPro, maxRankPro, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Miss PB (PRO)",
+ client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiffPro), rankPro, maxRankPro, gC_ModeNamesShort[mode]);
+ }
+ }
+ // Bonus Course NUB Times
+ else
+ {
+ if (firstTime)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - First Time",
+ client, course, GOKZ_FormatTime(runTime), rank, maxRank, gC_ModeNamesShort[mode]);
+ }
+ else if (pbDiff < 0)
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Beat PB",
+ client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(FloatAbs(pbDiff)), rank, maxRank, gC_ModeNamesShort[mode]);
+ }
+ else
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Time - Miss PB",
+ client, course, GOKZ_FormatTime(runTime), GOKZ_FormatTime(pbDiff), rank, maxRank, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+}
+
+void AnnounceNewRecord(int client, int course, int mode, int recordType)
+{
+ if (course == 0)
+ {
+ switch (recordType)
+ {
+ case RecordType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Record (NUB)", client, gC_ModeNamesShort[mode]);
+ }
+ case RecordType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Record (PRO)", client, gC_ModeNamesShort[mode]);
+ }
+ case RecordType_NubAndPro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Record (NUB and PRO)", client, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+ else
+ {
+ switch (recordType)
+ {
+ case RecordType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Record (NUB)", client, course, gC_ModeNamesShort[mode]);
+ }
+ case RecordType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Record (PRO)", client, course, gC_ModeNamesShort[mode]);
+ }
+ case RecordType_NubAndPro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "New Bonus Record (NUB and PRO)", client, course, course, gC_ModeNamesShort[mode]);
+ }
+ }
+ }
+
+ PlayBeatRecordSound(); // Play sound!
+}
+
+
+
+// =====[ MISSED RECORD TRACKING ]=====
+
+void ResetRecordMissed(int client)
+{
+ for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++)
+ {
+ gB_RecordMissed[client][timeType] = false;
+ }
+}
+
+void UpdateRecordMissed(int client)
+{
+ if (!GOKZ_GetTimerRunning(client) || gB_RecordMissed[client][TimeType_Nub] && gB_RecordMissed[client][TimeType_Pro])
+ {
+ return;
+ }
+
+ int course = GOKZ_GetCourse(client);
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+ float currentTime = GOKZ_GetTime(client);
+
+ bool nubRecordExists = gB_RecordExistsCache[course][mode][TimeType_Nub];
+ float nubRecordTime = gF_RecordTimesCache[course][mode][TimeType_Nub];
+ bool nubRecordMissed = gB_RecordMissed[client][TimeType_Nub];
+ bool proRecordExists = gB_RecordExistsCache[course][mode][TimeType_Pro];
+ float proRecordTime = gF_RecordTimesCache[course][mode][TimeType_Pro];
+ bool proRecordMissed = gB_RecordMissed[client][TimeType_Pro];
+
+ if (nubRecordExists && !nubRecordMissed && currentTime >= nubRecordTime)
+ {
+ gB_RecordMissed[client][TimeType_Nub] = true;
+
+ // Check if nub record is also the pro record, and call the forward appropriately
+ if (proRecordExists && FloatAbs(nubRecordTime - proRecordTime) < EPSILON)
+ {
+ gB_RecordMissed[client][TimeType_Pro] = true;
+ Call_OnRecordMissed(client, nubRecordTime, course, mode, Style_Normal, RecordType_NubAndPro);
+ }
+ else
+ {
+ Call_OnRecordMissed(client, nubRecordTime, course, mode, Style_Normal, RecordType_Nub);
+ }
+ }
+ else if (proRecordExists && !proRecordMissed && currentTime >= proRecordTime)
+ {
+ gB_RecordMissed[client][TimeType_Pro] = true;
+ Call_OnRecordMissed(client, proRecordTime, course, mode, Style_Normal, RecordType_Pro);
+ }
+}
+
+
+
+// =====[ MISSED PB TRACKING ]=====
+
+#define MISSED_PB_SOUND "buttons/button18.wav"
+
+void ResetPBMissed(int client)
+{
+ for (int timeType = 0; timeType < TIMETYPE_COUNT; timeType++)
+ {
+ gB_PBMissed[client][timeType] = false;
+ }
+}
+
+void UpdatePBMissed(int client)
+{
+ if (!GOKZ_GetTimerRunning(client) || gB_PBMissed[client][TimeType_Nub] && gB_PBMissed[client][TimeType_Pro])
+ {
+ return;
+ }
+
+ int course = GOKZ_GetCourse(client);
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+ float currentTime = GOKZ_GetTime(client);
+
+ bool nubPBExists = gB_PBExistsCache[client][course][mode][TimeType_Nub];
+ float nubPBTime = gF_PBTimesCache[client][course][mode][TimeType_Nub];
+ bool nubPBMissed = gB_PBMissed[client][TimeType_Nub];
+ bool proPBExists = gB_PBExistsCache[client][course][mode][TimeType_Pro];
+ float proPBTime = gF_PBTimesCache[client][course][mode][TimeType_Pro];
+ bool proPBMissed = gB_PBMissed[client][TimeType_Pro];
+
+ if (nubPBExists && !nubPBMissed && currentTime >= nubPBTime)
+ {
+ gB_PBMissed[client][TimeType_Nub] = true;
+
+ // Check if nub PB is also the pro PB, and call the forward appropriately
+ if (proPBExists && FloatAbs(nubPBTime - proPBTime) < EPSILON)
+ {
+ gB_PBMissed[client][TimeType_Pro] = true;
+ Call_OnPBMissed(client, nubPBTime, course, mode, Style_Normal, RecordType_NubAndPro);
+ }
+ else
+ {
+ Call_OnPBMissed(client, nubPBTime, course, mode, Style_Normal, RecordType_Nub);
+ }
+ }
+ else if (proPBExists && !proPBMissed && currentTime >= proPBTime)
+ {
+ gB_PBMissed[client][TimeType_Pro] = true;
+ Call_OnPBMissed(client, proPBTime, course, mode, Style_Normal, RecordType_Pro);
+ }
+}
+
+void DoPBMissedReport(int client, float pbTime, int recordType)
+{
+ switch (recordType)
+ {
+ case RecordType_Nub:GOKZ_PrintToChat(client, true, "%t", "Missed PB (NUB)", GOKZ_FormatTime(pbTime));
+ case RecordType_Pro:GOKZ_PrintToChat(client, true, "%t", "Missed PB (PRO)", GOKZ_FormatTime(pbTime));
+ case RecordType_NubAndPro:GOKZ_PrintToChat(client, true, "%t", "Missed PB (NUB and PRO)", GOKZ_FormatTime(pbTime));
+ }
+ GOKZ_EmitSoundToClient(client, MISSED_PB_SOUND, _, "Missed PB");
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-measure.sp b/sourcemod/scripting/gokz-measure.sp
new file mode 100644
index 0000000..2360625
--- /dev/null
+++ b/sourcemod/scripting/gokz-measure.sp
@@ -0,0 +1,82 @@
+#include <sourcemod>
+
+#include <sdktools>
+
+#include <gokz/core>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+/*
+ Lets players measure the distance between two points.
+ Credits to DaFox (https://forums.alliedmods.net/showthread.php?t=88830?t=88830)
+*/
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Measure",
+ author = "DanZay",
+ description = "Provides tools for measuring things",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-measure.txt"
+#define MEASURE_MIN_DIST 0.01
+int gI_BeamModel;
+bool gB_Measuring[MAXPLAYERS + 1];
+bool gB_MeasurePosSet[MAXPLAYERS + 1][2];
+float gF_MeasurePos[MAXPLAYERS + 1][2][3];
+float gF_MeasureNormal[MAXPLAYERS + 1][2][3];
+Handle gH_P2PRed[MAXPLAYERS + 1];
+Handle gH_P2PGreen[MAXPLAYERS + 1];
+
+#include "gokz-measure/measurer.sp"
+#include "gokz-measure/commands.sp"
+#include "gokz-measure/measure_menu.sp"
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-measure");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-measure.phrases");
+
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ gI_BeamModel = PrecacheModel("materials/sprites/laserbeam.vmt", true);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-measure/commands.sp b/sourcemod/scripting/gokz-measure/commands.sp
new file mode 100644
index 0000000..5fe3028
--- /dev/null
+++ b/sourcemod/scripting/gokz-measure/commands.sp
@@ -0,0 +1,49 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("+measure", CommandMeasureStart, "[KZ] Set the measure origin.");
+ RegConsoleCmd("-measure", CommandMeasureEnd, "[KZ] Set the measure origin.");
+ RegConsoleCmd("sm_measure", CommandMeasureMenu, "[KZ] Open the measurement menu.");
+ RegConsoleCmd("sm_measuremenu", CommandMeasureMenu, "[KZ] Open the measurement menu.");
+ RegConsoleCmd("sm_measureblock", CommandMeasureBlock, "[KZ] Measure the block distance.");
+}
+
+public Action CommandMeasureMenu(int client, int args)
+{
+ DisplayMeasureMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandMeasureStart(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ gB_Measuring[client] = true;
+ MeasureGetPos(client, 0);
+ return Plugin_Handled;
+}
+
+public Action CommandMeasureEnd(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ gB_Measuring[client] = false;
+ MeasureGetPos(client, 1);
+ MeasureDistance(client, MEASURE_MIN_DIST);
+ CreateTimer(4.9, Timer_DeletePoints, GetClientUserId(client));
+ return Plugin_Handled;
+}
+
+public Action CommandMeasureBlock(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ MeasureBlock(client);
+ CreateTimer(4.9, Timer_DeletePoints, GetClientUserId(client));
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-measure/measure_menu.sp b/sourcemod/scripting/gokz-measure/measure_menu.sp
new file mode 100644
index 0000000..cf9deb3
--- /dev/null
+++ b/sourcemod/scripting/gokz-measure/measure_menu.sp
@@ -0,0 +1,82 @@
+#define ITEM_INFO_POINT_A "a"
+#define ITEM_INFO_POINT_B "b"
+#define ITEM_INFO_GET_DISTANCE "get"
+#define ITEM_INFO_GET_BLOCK_DISTANCE "block"
+
+// =====[ PUBLIC ]=====
+
+void DisplayMeasureMenu(int client, bool reset = true)
+{
+ if (reset)
+ {
+ MeasureResetPos(client);
+ }
+
+ Menu menu = new Menu(MenuHandler_Measure);
+ menu.SetTitle("%T", "Measure Menu - Title", client);
+ MeasureMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public int MenuHandler_Measure(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_POINT_A, false))
+ {
+ MeasureGetPos(param1, 0);
+ }
+ else if (StrEqual(info, ITEM_INFO_POINT_B, false))
+ {
+ MeasureGetPos(param1, 1);
+ }
+ else if (StrEqual(info, ITEM_INFO_GET_DISTANCE, false))
+ {
+ MeasureDistance(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_GET_BLOCK_DISTANCE, false))
+ {
+ if (!MeasureBlock(param1))
+ {
+ DisplayMeasureMenu(param1, false);
+ }
+ }
+
+ DisplayMeasureMenu(param1, false);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ MeasureResetPos(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void MeasureMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ FormatEx(display, sizeof(display), "%T", "Measure Menu - Point A", client);
+ menu.AddItem(ITEM_INFO_POINT_A, display);
+ FormatEx(display, sizeof(display), "%T", "Measure Menu - Point B", client);
+ menu.AddItem(ITEM_INFO_POINT_B, display);
+ FormatEx(display, sizeof(display), "%T\n ", "Measure Menu - Get Distance", client);
+ menu.AddItem(ITEM_INFO_GET_DISTANCE, display);
+ FormatEx(display, sizeof(display), "%T", "Measure Menu - Get Block Distance", client);
+ menu.AddItem(ITEM_INFO_GET_BLOCK_DISTANCE, display);
+}
+
diff --git a/sourcemod/scripting/gokz-measure/measurer.sp b/sourcemod/scripting/gokz-measure/measurer.sp
new file mode 100644
index 0000000..f88e79c
--- /dev/null
+++ b/sourcemod/scripting/gokz-measure/measurer.sp
@@ -0,0 +1,231 @@
+// =====[ PUBLIC ]=====
+
+void MeasureGetPos(int client, int arg)
+{
+ float origin[3];
+ float angles[3];
+
+ GetClientEyePosition(client, origin);
+ GetClientEyeAngles(client, angles);
+
+ MeasureGetPosEx(client, arg, origin, angles);
+}
+
+void MeasureResetPos(int client)
+{
+ delete gH_P2PRed[client];
+ delete gH_P2PGreen[client];
+
+ gB_MeasurePosSet[client][0] = false;
+ gB_MeasurePosSet[client][1] = false;
+
+ gF_MeasurePos[client][0][0] = 0.0; // This is stupid.
+ gF_MeasurePos[client][0][1] = 0.0;
+ gF_MeasurePos[client][0][2] = 0.0;
+ gF_MeasurePos[client][1][0] = 0.0;
+ gF_MeasurePos[client][1][1] = 0.0;
+ gF_MeasurePos[client][1][2] = 0.0;
+}
+
+bool MeasureBlock(int client)
+{
+ float angles[3];
+ MeasureGetPos(client, 0);
+ GetVectorAngles(gF_MeasureNormal[client][0], angles);
+ MeasureGetPosEx(client, 1, gF_MeasurePos[client][0], angles);
+ AddVectors(gF_MeasureNormal[client][0], gF_MeasureNormal[client][1], angles);
+ if (GetVectorLength(angles, true) > EPSILON ||
+ FloatAbs(gF_MeasureNormal[client][0][2]) > EPSILON ||
+ FloatAbs(gF_MeasureNormal[client][1][2]) > EPSILON)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Measure Failure (Blocks not aligned)");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+ GOKZ_PrintToChat(client, true, "%t", "Block Measure Result", RoundFloat(GetVectorHorizontalDistance(gF_MeasurePos[client][0], gF_MeasurePos[client][1])));
+ MeasureBeam(client, gF_MeasurePos[client][0], gF_MeasurePos[client][1], 5.0, 0.2, 200, 200, 200);
+ return true;
+}
+
+bool MeasureDistance(int client, float minDistToMeasureBlock = -1.0)
+{
+ // Find Distance
+ if (gB_MeasurePosSet[client][0] && gB_MeasurePosSet[client][1])
+ {
+ float horizontalDist = GetVectorHorizontalDistance(gF_MeasurePos[client][0], gF_MeasurePos[client][1]);
+ float effectiveDist = CalcEffectiveDistance(gF_MeasurePos[client][0], gF_MeasurePos[client][1]);
+ float verticalDist = gF_MeasurePos[client][1][2] - gF_MeasurePos[client][0][2];
+ if (minDistToMeasureBlock >= 0.0 && (horizontalDist <= minDistToMeasureBlock && verticalDist <= minDistToMeasureBlock))
+ {
+ return MeasureBlock(client);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Measure Result", horizontalDist, effectiveDist, verticalDist);
+ MeasureBeam(client, gF_MeasurePos[client][0], gF_MeasurePos[client][1], 5.0, 0.2, 200, 200, 200);
+ }
+ return true;
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Measure Failure (Points Not Set)");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+}
+
+// =====[ TIMERS ]=====
+
+public Action Timer_P2PRed(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client))
+ {
+ P2PXBeam(client, 0);
+ }
+ return Plugin_Continue;
+}
+
+public Action Timer_P2PGreen(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client))
+ {
+ P2PXBeam(client, 1);
+ }
+ return Plugin_Continue;
+}
+
+public Action Timer_DeletePoints(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (!gB_Measuring[client])
+ {
+ MeasureResetPos(client);
+ }
+ return Plugin_Continue;
+}
+
+
+// =====[ PRIVATES ]=====
+static void P2PXBeam(int client, int arg)
+{
+ float Origin0[3];
+ float Origin1[3];
+ float Origin2[3];
+ float Origin3[3];
+
+ Origin0[0] = (gF_MeasurePos[client][arg][0] + 8.0);
+ Origin0[1] = (gF_MeasurePos[client][arg][1] + 8.0);
+ Origin0[2] = gF_MeasurePos[client][arg][2];
+
+ Origin1[0] = (gF_MeasurePos[client][arg][0] - 8.0);
+ Origin1[1] = (gF_MeasurePos[client][arg][1] - 8.0);
+ Origin1[2] = gF_MeasurePos[client][arg][2];
+
+ Origin2[0] = (gF_MeasurePos[client][arg][0] + 8.0);
+ Origin2[1] = (gF_MeasurePos[client][arg][1] - 8.0);
+ Origin2[2] = gF_MeasurePos[client][arg][2];
+
+ Origin3[0] = (gF_MeasurePos[client][arg][0] - 8.0);
+ Origin3[1] = (gF_MeasurePos[client][arg][1] + 8.0);
+ Origin3[2] = gF_MeasurePos[client][arg][2];
+
+ if (arg == 0)
+ {
+ MeasureBeam(client, Origin0, Origin1, 0.97, 0.2, 0, 255, 0);
+ MeasureBeam(client, Origin2, Origin3, 0.97, 0.2, 0, 255, 0);
+ }
+ else
+ {
+ MeasureBeam(client, Origin0, Origin1, 0.97, 0.2, 255, 0, 0);
+ MeasureBeam(client, Origin2, Origin3, 0.97, 0.2, 255, 0, 0);
+ }
+}
+
+static void MeasureGetPosEx(int client, int arg, float origin[3], float angles[3])
+{
+ Handle trace = TR_TraceRayFilterEx(origin, angles, MASK_PLAYERSOLID, RayType_Infinite, TraceEntityFilterPlayers, client);
+
+ if (!TR_DidHit(trace))
+ {
+ delete trace;
+ GOKZ_PrintToChat(client, true, "%t", "Measure Failure (Not Aiming at Solid)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ TR_GetEndPosition(gF_MeasurePos[client][arg], trace);
+ TR_GetPlaneNormal(trace, gF_MeasureNormal[client][arg]);
+ delete trace;
+
+ if (arg == 0)
+ {
+ delete gH_P2PRed[client];
+ gB_MeasurePosSet[client][0] = true;
+ gH_P2PRed[client] = CreateTimer(1.0, Timer_P2PRed, GetClientUserId(client), TIMER_REPEAT);
+ P2PXBeam(client, 0);
+ }
+ else
+ {
+ delete gH_P2PGreen[client];
+ gH_P2PGreen[client] = null;
+ gB_MeasurePosSet[client][1] = true;
+ P2PXBeam(client, 1);
+ gH_P2PGreen[client] = CreateTimer(1.0, Timer_P2PGreen, GetClientUserId(client), TIMER_REPEAT);
+ }
+}
+
+static void MeasureBeam(int client, float vecStart[3], float vecEnd[3], float life, float width, int r, int g, int b)
+{
+ TE_Start("BeamPoints");
+ TE_WriteNum("m_nModelIndex", gI_BeamModel);
+ TE_WriteNum("m_nHaloIndex", 0);
+ TE_WriteNum("m_nStartFrame", 0);
+ TE_WriteNum("m_nFrameRate", 0);
+ TE_WriteFloat("m_fLife", life);
+ TE_WriteFloat("m_fWidth", width);
+ TE_WriteFloat("m_fEndWidth", width);
+ TE_WriteNum("m_nFadeLength", 0);
+ TE_WriteFloat("m_fAmplitude", 0.0);
+ TE_WriteNum("m_nSpeed", 0);
+ TE_WriteNum("r", r);
+ TE_WriteNum("g", g);
+ TE_WriteNum("b", b);
+ TE_WriteNum("a", 255);
+ TE_WriteNum("m_nFlags", 0);
+ TE_WriteVector("m_vecStartPoint", vecStart);
+ TE_WriteVector("m_vecEndPoint", vecEnd);
+ TE_SendToClient(client);
+}
+
+// Calculates the minimum equivalent jumpstat distance to go between the two points
+static float CalcEffectiveDistance(const float pointA[3], const float pointB[3])
+{
+ float Ax = FloatMin(pointA[0], pointB[0]);
+ float Bx = FloatMax(pointA[0], pointB[0]);
+ float Ay = FloatMin(pointA[1], pointB[1]);
+ float By = FloatMax(pointA[1], pointB[1]);
+
+ if (Bx - Ax < 32.0)
+ {
+ Ax = Bx;
+ }
+ else
+ {
+ Ax = Ax + 16.0;
+ Bx = Bx - 16.0;
+ }
+
+ if (By - Ay < 32.0)
+ {
+ Ay = By;
+ }
+ else
+ {
+ Ay = Ay + 16.0;
+ By = By - 16.0;
+ }
+
+ return SquareRoot(Pow(Ax - Bx, 2.0) + Pow(Ay - By, 2.0)) + 32.0;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-mode-kztimer.sp b/sourcemod/scripting/gokz-mode-kztimer.sp
new file mode 100644
index 0000000..272652b
--- /dev/null
+++ b/sourcemod/scripting/gokz-mode-kztimer.sp
@@ -0,0 +1,709 @@
+#include <sourcemod>
+
+#include <sdkhooks>
+#include <sdktools>
+#include <dhooks>
+
+#include <movementapi>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/core>
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Mode - KZTimer",
+ author = "DanZay",
+ description = "KZTimer mode for GOKZ",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-mode-kztimer.txt"
+
+#define MODE_VERSION 217
+#define DUCK_SPEED_NORMAL 8.0
+#define PRE_VELMOD_MAX 1.104 // Calculated 276/250
+#define PERF_SPEED_CAP 380.0
+
+float gF_ModeCVarValues[MODECVAR_COUNT] =
+{
+ 6.5, // sv_accelerate
+ 0.0, // sv_accelerate_use_weapon_speed
+ 100.0, // sv_airaccelerate
+ 30.0, // sv_air_max_wishspeed
+ 1.0, // sv_enablebunnyhopping
+ 5.0, // sv_friction
+ 800.0, // sv_gravity
+ 301.993377, // sv_jump_impulse
+ 1.0, // sv_ladder_scale_speed
+ 0.0, // sv_ledge_mantle_helper
+ 320.0, // sv_maxspeed
+ 2000.0, // sv_maxvelocity
+ 0.0, // sv_staminajumpcost
+ 0.0, // sv_staminalandcost
+ 0.0, // sv_staminamax
+ 0.0, // sv_staminarecoveryrate
+ 0.7, // sv_standable_normal
+ 0.0, // sv_timebetweenducks
+ 0.7, // sv_walkable_normal
+ 10.0, // sv_wateraccelerate
+ 0.8, // sv_water_movespeed_multiplier
+ 0.0, // sv_water_swim_mode
+ 0.0, // sv_weapon_encumbrance_per_item
+ 0.0 // sv_weapon_encumbrance_scale
+};
+
+bool gB_GOKZCore;
+ConVar gCV_ModeCVar[MODECVAR_COUNT];
+float gF_PreVelMod[MAXPLAYERS + 1];
+float gF_PreVelModLastChange[MAXPLAYERS + 1];
+float gF_RealPreVelMod[MAXPLAYERS + 1];
+int gI_PreTickCounter[MAXPLAYERS + 1];
+Handle gH_GetPlayerMaxSpeed;
+DynamicDetour gH_CanUnduck;
+int gI_TickCount[MAXPLAYERS + 1];
+DynamicDetour gH_AirAccelerate;
+int gI_OldButtons[MAXPLAYERS + 1];
+int gI_OldFlags[MAXPLAYERS + 1];
+bool gB_OldOnGround[MAXPLAYERS + 1];
+float gF_OldVelocity[MAXPLAYERS + 1][3];
+bool gB_Jumpbugged[MAXPLAYERS + 1];
+int gI_OffsetCGameMovement_player;
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public void OnPluginStart()
+{
+ if (FloatAbs(1.0 / GetTickInterval() - 128.0) > EPSILON)
+ {
+ SetFailState("gokz-mode-kztimer only supports 128 tickrate servers.");
+ }
+ HookEvents();
+ CreateConVars();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ if (LibraryExists("gokz-core"))
+ {
+ gB_GOKZCore = true;
+ GOKZ_SetModeLoaded(Mode_KZTimer, true, MODE_VERSION);
+ }
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ OnClientPutInServer(client);
+ }
+ }
+}
+
+public void OnPluginEnd()
+{
+ if (gB_GOKZCore)
+ {
+ GOKZ_SetModeLoaded(Mode_KZTimer, false);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ else if (StrEqual(name, "gokz-core"))
+ {
+ gB_GOKZCore = true;
+ GOKZ_SetModeLoaded(Mode_KZTimer, true, MODE_VERSION);
+ }
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZCore = gB_GOKZCore && !StrEqual(name, "gokz-core");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ if (IsValidClient(client))
+ {
+ HookClientEvents(client);
+ }
+ if (IsUsingMode(client))
+ {
+ ReplicateConVars(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])
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ RemoveCrouchJumpBind(player, buttons);
+ gF_RealPreVelMod[player.ID] = CalcPrestrafeVelMod(player);
+ ReduceDuckSlowdown(player);
+ FixWaterBoost(player, buttons);
+ FixDisplacementStuck(player);
+
+ gB_Jumpbugged[player.ID] = false;
+ gI_OldButtons[player.ID] = buttons;
+ gI_OldFlags[player.ID] = GetEntityFlags(client);
+ gB_OldOnGround[player.ID] = Movement_GetOnGround(client);
+ gI_TickCount[player.ID] = tickcount;
+ Movement_GetVelocity(client, gF_OldVelocity[client]);
+ return Plugin_Continue;
+}
+
+public MRESReturn DHooks_OnGetPlayerMaxSpeed(int client, Handle hReturn)
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return MRES_Ignored;
+ }
+
+ DHookSetReturn(hReturn, SPEED_NORMAL * gF_RealPreVelMod[client]);
+ return MRES_Supercede;
+}
+
+public MRESReturn DHooks_OnAirAccelerate_Pre(Address pThis, DHookParam hParams)
+{
+ int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player);
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return MRES_Ignored;
+ }
+
+ // NOTE: Prestrafing changes GetPlayerMaxSpeed, which changes
+ // air acceleration, so remove gF_PreVelMod[client] from wishspeed/maxspeed.
+ // This also applies to when the player is ducked: their wishspeed is
+ // 85 and with prestrafing can be ~93.
+ float wishspeed = DHookGetParam(hParams, 2);
+ if (gF_PreVelMod[client] > 1.0)
+ {
+ DHookSetParam(hParams, 2, wishspeed / gF_PreVelMod[client]);
+ return MRES_ChangedHandled;
+ }
+
+ return MRES_Ignored;
+}
+
+public MRESReturn DHooks_OnCanUnduck_Pre(Address pThis, DHookReturn hReturn)
+{
+ int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player);
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return MRES_Ignored;
+ }
+ // Just landed fully ducked, you can't unduck.
+ if (Movement_GetLandingTick(client) == (gI_TickCount[client] - 1) && GetEntPropFloat(client, Prop_Send, "m_flDuckAmount") >= 1.0 && GetEntProp(client, Prop_Send, "m_bDucked"))
+ {
+ hReturn.Value = false;
+ return MRES_Supercede;
+ }
+ return MRES_Ignored;
+}
+
+public void SDKHook_OnClientPreThink_Post(int client)
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return;
+ }
+
+ // Don't tweak convars if GOKZ isn't running
+ if (gB_GOKZCore)
+ {
+ TweakConVars();
+ }
+}
+
+public Action Movement_OnCategorizePositionPost(int client, float origin[3], float velocity[3])
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+ return SlopeFix(client, origin, velocity);
+}
+
+public Action Movement_OnJumpPre(int client, float origin[3], float velocity[3])
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ return TweakJump(player, velocity);
+}
+
+public Action Movement_OnJumpPost(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore)
+ {
+ player.GOKZHitPerf = player.HitPerf;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+ return Plugin_Continue;
+}
+
+public void Movement_OnStopTouchGround(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore)
+ {
+ player.GOKZHitPerf = player.HitPerf;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+}
+
+public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype)
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore && newMovetype == MOVETYPE_WALK)
+ {
+ player.GOKZHitPerf = false;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) && newValue == Mode_KZTimer)
+ {
+ ReplicateConVars(client);
+ }
+}
+
+public void GOKZ_OnCountedTeleport_Post(int client)
+{
+ KZPlayer player = KZPlayer(client);
+ ResetPrestrafeVelMod(player);
+}
+
+
+
+// =====[ GENERAL ]=====
+
+bool IsUsingMode(int client)
+{
+ // If GOKZ core isn't loaded, then apply mode at all times
+ return !gB_GOKZCore || GOKZ_GetCoreOption(client, Option_Mode) == Mode_KZTimer;
+}
+
+void HookEvents()
+{
+ GameData gameData = LoadGameConfigFile("movementapi.games");
+ if (gameData == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find movementapi.games config");
+ }
+
+ int offset = gameData.GetOffset("GetPlayerMaxSpeed");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get GetPlayerMaxSpeed offset");
+ }
+ gH_GetPlayerMaxSpeed = DHookCreate(offset, HookType_Entity, ReturnType_Float, ThisPointer_CBaseEntity, DHooks_OnGetPlayerMaxSpeed);
+
+ gH_AirAccelerate = DynamicDetour.FromConf(gameData, "CGameMovement::AirAccelerate");
+ if (gH_AirAccelerate == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find CGameMovement::AirAccelerate function signature");
+ }
+
+ if (!gH_AirAccelerate.Enable(Hook_Pre, DHooks_OnAirAccelerate_Pre))
+ {
+ SetFailState("Failed to enable detour on CGameMovement::AirAccelerate");
+ }
+
+ char buffer[16];
+ if (!gameData.GetKeyValue("CGameMovement::player", buffer, sizeof(buffer)))
+ {
+ SetFailState("Failed to get CGameMovement::player offset.");
+ }
+ gI_OffsetCGameMovement_player = StringToInt(buffer);
+
+ gameData = LoadGameConfigFile("gokz-core.games");
+ gH_CanUnduck = DynamicDetour.FromConf(gameData, "CCSGameMovement::CanUnduck");
+ if (gH_CanUnduck == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find CCSGameMovement::CanUnduck function signature");
+ }
+
+ if (!gH_CanUnduck.Enable(Hook_Pre, DHooks_OnCanUnduck_Pre))
+ {
+ SetFailState("Failed to enable detour on CCSGameMovement::CanUnduck");
+ }
+ delete gameData;
+}
+
+// =====[ CONVARS ]=====
+
+void CreateConVars()
+{
+ for (int cvar = 0; cvar < MODECVAR_COUNT; cvar++)
+ {
+ gCV_ModeCVar[cvar] = FindConVar(gC_ModeCVars[cvar]);
+ }
+}
+
+void TweakConVars()
+{
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i].FloatValue = gF_ModeCVarValues[i];
+ }
+}
+
+void ReplicateConVars(int client)
+{
+ // Replicate convars only when player changes mode in GOKZ
+ // so that lagg isn't caused by other players using other
+ // modes, and also as an optimisation.
+
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i].ReplicateToClient(client, FloatToStringEx(gF_ModeCVarValues[i]));
+ }
+}
+
+
+
+// =====[ VELOCITY MODIFIER ]=====
+
+void HookClientEvents(int client)
+{
+ DHookEntity(gH_GetPlayerMaxSpeed, true, client);
+ SDKHook(client, SDKHook_PreThinkPost, SDKHook_OnClientPreThink_Post);
+}
+
+// Adapted from KZTimerGlobal
+float CalcPrestrafeVelMod(KZPlayer player)
+{
+ if (!player.OnGround)
+ {
+ return gF_PreVelMod[player.ID];
+ }
+
+ if (!player.Turning)
+ {
+ if (GetEngineTime() - gF_PreVelModLastChange[player.ID] > 0.2)
+ {
+ gF_PreVelMod[player.ID] = 1.0;
+ gF_PreVelModLastChange[player.ID] = GetEngineTime();
+ }
+ else if (gF_PreVelMod[player.ID] > PRE_VELMOD_MAX + 0.007)
+ {
+ return PRE_VELMOD_MAX - 0.001; // Returning without setting the variable is intentional
+ }
+ }
+ else if ((player.Buttons & IN_MOVELEFT || player.Buttons & IN_MOVERIGHT) && player.Speed > 248.9)
+ {
+ float increment = 0.0009;
+ if (gF_PreVelMod[player.ID] > 1.04)
+ {
+ increment = 0.001;
+ }
+
+ bool forwards = GetClientMovingDirection(player.ID, false) > 0.0;
+
+ if ((player.Buttons & IN_MOVERIGHT && player.TurningRight || player.TurningLeft && !forwards)
+ || (player.Buttons & IN_MOVELEFT && player.TurningLeft || player.TurningRight && !forwards))
+ {
+ gI_PreTickCounter[player.ID]++;
+
+ if (gI_PreTickCounter[player.ID] < 75)
+ {
+ gF_PreVelMod[player.ID] += increment;
+ if (gF_PreVelMod[player.ID] > PRE_VELMOD_MAX)
+ {
+ if (gF_PreVelMod[player.ID] > PRE_VELMOD_MAX + 0.007)
+ {
+ gF_PreVelMod[player.ID] = PRE_VELMOD_MAX - 0.001;
+ }
+ else
+ {
+ gF_PreVelMod[player.ID] -= 0.007;
+ }
+ }
+ gF_PreVelMod[player.ID] += increment;
+ }
+ else
+ {
+ gF_PreVelMod[player.ID] -= 0.0045;
+ gI_PreTickCounter[player.ID] -= 2;
+
+ if (gF_PreVelMod[player.ID] < 1.0)
+ {
+ gF_PreVelMod[player.ID] = 1.0;
+ gI_PreTickCounter[player.ID] = 0;
+ }
+ }
+ }
+ else
+ {
+ gF_PreVelMod[player.ID] -= 0.04;
+
+ if (gF_PreVelMod[player.ID] < 1.0)
+ {
+ gF_PreVelMod[player.ID] = 1.0;
+ }
+ }
+
+ gF_PreVelModLastChange[player.ID] = GetEngineTime();
+ }
+ else
+ {
+ gI_PreTickCounter[player.ID] = 0;
+ return 1.0; // Returning without setting the variable is intentional
+ }
+
+ return gF_PreVelMod[player.ID];
+}
+
+// Adapted from KZTimerGlobal
+float GetClientMovingDirection(int client, bool ladder)
+{
+ float fVelocity[3];
+ GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", fVelocity);
+
+ float fEyeAngles[3];
+ GetClientEyeAngles(client, fEyeAngles);
+
+ if (fEyeAngles[0] > 70.0)fEyeAngles[0] = 70.0;
+ if (fEyeAngles[0] < -70.0)fEyeAngles[0] = -70.0;
+
+ float fViewDirection[3];
+
+ if (ladder)
+ {
+ GetEntPropVector(client, Prop_Send, "m_vecLadderNormal", fViewDirection);
+ }
+ else
+ {
+ GetAngleVectors(fEyeAngles, fViewDirection, NULL_VECTOR, NULL_VECTOR);
+ }
+
+ NormalizeVector(fVelocity, fVelocity);
+ NormalizeVector(fViewDirection, fViewDirection);
+
+ float direction = GetVectorDotProduct(fVelocity, fViewDirection);
+ if (ladder)
+ {
+ direction = direction * -1;
+ }
+ return direction;
+}
+
+void ResetPrestrafeVelMod(KZPlayer player)
+{
+ gF_PreVelMod[player.ID] = 1.0;
+ gI_PreTickCounter[player.ID] = 0;
+}
+
+
+
+// =====[ SLOPEFIX ]=====
+
+// ORIGINAL AUTHORS : Mev & Blacky
+// URL : https://forums.alliedmods.net/showthread.php?p=2322788
+// NOTE : Modified by DanZay for this plugin
+
+Action SlopeFix(int client, float origin[3], float velocity[3])
+{
+ KZPlayer player = KZPlayer(client);
+ // Check if player landed on the ground
+ if (Movement_GetOnGround(client) && !gB_OldOnGround[client])
+ {
+ float vMins[] = {-16.0, -16.0, 0.0};
+ // Always use ducked hull as the real hull size isn't updated yet.
+ // Might cause slight issues in extremely rare scenarios.
+ float vMaxs[] = {16.0, 16.0, 54.0};
+
+ float vEndPos[3];
+ vEndPos[0] = origin[0];
+ vEndPos[1] = origin[1];
+ vEndPos[2] = origin[2] - gF_ModeCVarValues[ModeCVar_MaxVelocity];
+
+ // Set up and do tracehull to find out if the player landed on a slope
+ TR_TraceHullFilter(origin, vEndPos, vMins, vMaxs, MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitSelf, client);
+
+ if (TR_DidHit())
+ {
+ // Gets the normal vector of the surface under the player
+ float vPlane[3], vLast[3];
+ player.GetLandingVelocity(vLast);
+ TR_GetPlaneNormal(null, vPlane);
+
+ // Make sure it's not flat ground and not a surf ramp (1.0 = flat ground, < 0.7 = surf ramp)
+ if (0.7 <= vPlane[2] < 1.0)
+ {
+ /*
+ Copy the ClipVelocity function from sdk2013
+ (https://mxr.alliedmods.net/hl2sdk-sdk2013/source/game/shared/gamemovement.cpp#3145)
+ With some minor changes to make it actually work
+ */
+
+ float fBackOff = GetVectorDotProduct(vLast, vPlane);
+
+ float change, vVel[3];
+ for (int i; i < 2; i++)
+ {
+ change = vPlane[i] * fBackOff;
+ vVel[i] = vLast[i] - change;
+ }
+
+ float fAdjust = GetVectorDotProduct(vVel, vPlane);
+ if (fAdjust < 0.0)
+ {
+ for (int i; i < 2; i++)
+ {
+ vVel[i] -= (vPlane[i] * fAdjust);
+ }
+ }
+
+ vVel[2] = 0.0;
+ vLast[2] = 0.0;
+
+ // Make sure the player is going down a ramp by checking if they actually will gain speed from the boost
+ if (GetVectorLength(vVel) > GetVectorLength(vLast))
+ {
+ CopyVector(vVel, velocity);
+ player.SetLandingVelocity(velocity);
+ return Plugin_Changed;
+ }
+ }
+ }
+ }
+ return Plugin_Continue;
+}
+
+public bool TraceRayDontHitSelf(int entity, int mask, any data)
+{
+ return entity != data && !(0 < entity <= MaxClients);
+}
+
+
+
+// =====[ JUMPING ]=====
+
+Action TweakJump(KZPlayer player, float velocity[3])
+{
+ if (player.HitPerf)
+ {
+ if (GetVectorHorizontalLength(velocity) > PERF_SPEED_CAP)
+ {
+ SetVectorHorizontalLength(velocity, PERF_SPEED_CAP);
+ return Plugin_Changed;
+ }
+ }
+ return Plugin_Continue;
+}
+// =====[ OTHER ]=====
+
+void FixWaterBoost(KZPlayer player, int buttons)
+{
+ if (GetEntProp(player.ID, Prop_Send, "m_nWaterLevel") >= 2) // WL_Waist = 2
+ {
+ // If duck is being pressed and we're not already ducking or on ground
+ if (GetEntityFlags(player.ID) & (FL_DUCKING | FL_ONGROUND) == 0
+ && buttons & IN_DUCK && ~gI_OldButtons[player.ID] & IN_DUCK)
+ {
+ float newOrigin[3];
+ Movement_GetOrigin(player.ID, newOrigin);
+ newOrigin[2] += 9.0;
+
+ TR_TraceHullFilter(newOrigin, newOrigin, view_as<float>({-16.0, -16.0, 0.0}), view_as<float>({16.0, 16.0, 54.0}), MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+ if (!TR_DidHit())
+ {
+ TeleportEntity(player.ID, newOrigin, NULL_VECTOR, NULL_VECTOR);
+ }
+ }
+ }
+}
+
+void FixDisplacementStuck(KZPlayer player)
+{
+ int flags = GetEntityFlags(player.ID);
+ bool unducked = ~flags & FL_DUCKING && gI_OldFlags[player.ID] & FL_DUCKING;
+
+ float standingMins[] = {-16.0, -16.0, 0.0};
+ float standingMaxs[] = {16.0, 16.0, 72.0};
+
+ if (unducked)
+ {
+ // check if we're stuck after unducking and if we're stuck then force duck
+ float origin[3];
+ Movement_GetOrigin(player.ID, origin);
+ TR_TraceHullFilter(origin, origin, standingMins, standingMaxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+
+ if (TR_DidHit())
+ {
+ player.SetVelocity(gF_OldVelocity[player.ID]);
+ SetEntProp(player.ID, Prop_Send, "m_bDucking", true);
+ }
+ }
+}
+
+void RemoveCrouchJumpBind(KZPlayer player, int &buttons)
+{
+ if (player.OnGround && buttons & IN_JUMP && !(gI_OldButtons[player.ID] & IN_JUMP) && !(gI_OldButtons[player.ID] & IN_DUCK))
+ {
+ buttons &= ~IN_DUCK;
+ }
+}
+
+void ReduceDuckSlowdown(KZPlayer player)
+{
+ if (GetEntProp(player.ID, Prop_Data, "m_afButtonReleased") & IN_DUCK)
+ {
+ Movement_SetDuckSpeed(player.ID, DUCK_SPEED_NORMAL);
+ }
+}
diff --git a/sourcemod/scripting/gokz-mode-simplekz.sp b/sourcemod/scripting/gokz-mode-simplekz.sp
new file mode 100644
index 0000000..99d76ac
--- /dev/null
+++ b/sourcemod/scripting/gokz-mode-simplekz.sp
@@ -0,0 +1,846 @@
+#include <sourcemod>
+
+#include <sdkhooks>
+#include <sdktools>
+#include <dhooks>
+
+#include <movementapi>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/core>
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Mode - SimpleKZ",
+ author = "DanZay",
+ description = "SimpleKZ mode for GOKZ",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-mode-simplekz.txt"
+
+#define MODE_VERSION 21
+#define PS_MAX_REWARD_TURN_RATE 0.703125 // Degrees per tick (90 degrees per second)
+#define PS_MAX_TURN_RATE_DECREMENT 0.015625 // Degrees per tick (2 degrees per second)
+#define PS_SPEED_MAX 26.54321 // Units
+#define PS_SPEED_INCREMENT 0.35 // Units per tick
+#define PS_SPEED_DECREMENT_MIDAIR 0.2824 // Units per tick (lose PS_SPEED_MAX in 0 offset jump i.e. 94 ticks)
+#define PS_GRACE_TICKS 3 // No. of ticks allowed to fail prestrafe checks when prestrafing - helps players with low fps
+#define DUCK_SPEED_NORMAL 8.0
+#define DUCK_SPEED_MINIMUM 6.0234375 // Equal to if you just ducked/unducked for the first time in a while
+
+float gF_ModeCVarValues[MODECVAR_COUNT] =
+{
+ 6.5, // sv_accelerate
+ 0.0, // sv_accelerate_use_weapon_speed
+ 100.0, // sv_airaccelerate
+ 30.0, // sv_air_max_wishspeed
+ 1.0, // sv_enablebunnyhopping
+ 5.2, // sv_friction
+ 800.0, // sv_gravity
+ 301.993377, // sv_jump_impulse
+ 1.0, // sv_ladder_scale_speed
+ 0.0, // sv_ledge_mantle_helper
+ 320.0, // sv_maxspeed
+ 3500.0, // sv_maxvelocity
+ 0.0, // sv_staminajumpcost
+ 0.0, // sv_staminalandcost
+ 0.0, // sv_staminamax
+ 0.0, // sv_staminarecoveryrate
+ 0.7, // sv_standable_normal
+ 0.0, // sv_timebetweenducks
+ 0.7, // sv_walkable_normal
+ 10.0, // sv_wateraccelerate
+ 0.8, // sv_water_movespeed_multiplier
+ 0.0, // sv_water_swim_mode
+ 0.0, // sv_weapon_encumbrance_per_item
+ 0.0 // sv_weapon_encumbrance_scale
+};
+
+bool gB_GOKZCore;
+ConVar gCV_ModeCVar[MODECVAR_COUNT];
+bool gB_HitTweakedPerf[MAXPLAYERS + 1];
+int gI_Cmdnum[MAXPLAYERS + 1];
+float gF_PSBonusSpeed[MAXPLAYERS + 1];
+float gF_PSVelMod[MAXPLAYERS + 1];
+float gF_PSVelModLanding[MAXPLAYERS + 1];
+bool gB_PSTurningLeft[MAXPLAYERS + 1];
+float gF_PSTurnRate[MAXPLAYERS + 1];
+int gI_PSTicksSinceIncrement[MAXPLAYERS + 1];
+Handle gH_GetPlayerMaxSpeed;
+DynamicDetour gH_CanUnduck;
+int gI_TickCount[MAXPLAYERS + 1];
+DynamicDetour gH_AirAccelerate;
+int gI_OldButtons[MAXPLAYERS + 1];
+int gI_OldFlags[MAXPLAYERS + 1];
+bool gB_OldOnGround[MAXPLAYERS + 1];
+float gF_OldOrigin[MAXPLAYERS + 1][3];
+float gF_OldAngles[MAXPLAYERS + 1][3];
+float gF_OldVelocity[MAXPLAYERS + 1][3];
+int gI_LastJumpButtonCmdnum[MAXPLAYERS + 1];
+int gI_OffsetCGameMovement_player;
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public void OnPluginStart()
+{
+ if (FloatAbs(1.0 / GetTickInterval() - 128.0) > EPSILON)
+ {
+ SetFailState("gokz-mode-simplekz only supports 128 tickrate servers.");
+ }
+ HookEvents();
+ CreateConVars();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ if (LibraryExists("gokz-core"))
+ {
+ gB_GOKZCore = true;
+ GOKZ_SetModeLoaded(Mode_SimpleKZ, true, MODE_VERSION);
+ }
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ OnClientPutInServer(client);
+ }
+ }
+}
+
+public void OnPluginEnd()
+{
+ if (gB_GOKZCore)
+ {
+ GOKZ_SetModeLoaded(Mode_SimpleKZ, false);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ else if (StrEqual(name, "gokz-core"))
+ {
+ gB_GOKZCore = true;
+ GOKZ_SetModeLoaded(Mode_SimpleKZ, true, MODE_VERSION);
+ }
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZCore = gB_GOKZCore && !StrEqual(name, "gokz-core");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ ResetClient(client);
+ if (IsValidClient(client))
+ {
+ HookClientEvents(client);
+ }
+ if (IsUsingMode(client))
+ {
+ ReplicateConVars(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])
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ RemoveCrouchJumpBind(player, buttons);
+ ReduceDuckSlowdown(player);
+ CalcPrestrafeVelMod(player, angles);
+ FixWaterBoost(player, buttons);
+ FixDisplacementStuck(player);
+
+ gB_HitTweakedPerf[player.ID] = false;
+ gI_Cmdnum[player.ID] = cmdnum;
+ gI_OldButtons[player.ID] = buttons;
+ gI_OldFlags[player.ID] = GetEntityFlags(player.ID);
+ gB_OldOnGround[player.ID] = player.OnGround;
+ gI_TickCount[player.ID] = tickcount;
+ player.GetOrigin(gF_OldOrigin[player.ID]);
+ player.GetEyeAngles(gF_OldAngles[player.ID]);
+ player.GetVelocity(gF_OldVelocity[player.ID]);
+
+ return Plugin_Continue;
+}
+
+public MRESReturn DHooks_OnGetPlayerMaxSpeed(int client, Handle hReturn)
+{
+ if (!IsUsingMode(client))
+ {
+ return MRES_Ignored;
+ }
+ DHookSetReturn(hReturn, SPEED_NORMAL * gF_PSVelMod[client]);
+ return MRES_Supercede;
+}
+
+public MRESReturn DHooks_OnAirAccelerate_Pre(Address pThis, DHookParam hParams)
+{
+ int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player);
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return MRES_Ignored;
+ }
+
+ // NOTE: Prestrafing changes GetPlayerMaxSpeed, which changes
+ // air acceleration, so remove gF_PreVelMod[client] from wishspeed/maxspeed.
+ // This also applies to when the player is ducked: their wishspeed is
+ // 85 and with prestrafing can be ~93.
+ float wishspeed = DHookGetParam(hParams, 2);
+ if (gF_PSVelMod[client] > 1.0)
+ {
+ DHookSetParam(hParams, 2, wishspeed / gF_PSVelMod[client]);
+ return MRES_ChangedHandled;
+ }
+
+ return MRES_Ignored;
+}
+
+public MRESReturn DHooks_OnCanUnduck_Pre(Address pThis, DHookReturn hReturn)
+{
+ int client = GOKZGetClientFromGameMovementAddress(pThis, gI_OffsetCGameMovement_player);
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return MRES_Ignored;
+ }
+ // Just landed fully ducked, you can't unduck.
+ if (Movement_GetLandingTick(client) == (gI_TickCount[client] - 1) && GetEntPropFloat(client, Prop_Send, "m_flDuckAmount") >= 1.0 && GetEntProp(client, Prop_Send, "m_bDucked"))
+ {
+ hReturn.Value = false;
+ return MRES_Supercede;
+ }
+ return MRES_Ignored;
+}
+
+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) || !IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return;
+ }
+
+ if (buttons & IN_JUMP)
+ {
+ gI_LastJumpButtonCmdnum[client] = cmdnum;
+ }
+}
+
+public void SDKHook_OnClientPreThink_Post(int client)
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return;
+ }
+
+ // Don't tweak convars if GOKZ isn't running
+ if (gB_GOKZCore)
+ {
+ TweakConVars();
+ }
+}
+
+public void Movement_OnStartTouchGround(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ gF_PSVelModLanding[player.ID] = gF_PSVelMod[player.ID];
+}
+
+public Action Movement_OnJumpPre(int client, float origin[3], float velocity[3])
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ return TweakJump(player, origin, velocity);
+}
+
+public Action Movement_OnCategorizePositionPost(int client, float origin[3], float velocity[3])
+{
+ if (!IsPlayerAlive(client) || !IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+ return SlopeFix(client, origin, velocity);
+}
+
+public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore && newMovetype == MOVETYPE_WALK)
+ {
+ player.GOKZHitPerf = false;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) && newValue == Mode_SimpleKZ)
+ {
+ ReplicateConVars(client);
+ }
+}
+
+public void GOKZ_OnCountedTeleport_Post(int client)
+{
+ ResetClient(client);
+}
+
+
+
+// =====[ GENERAL ]=====
+
+bool IsUsingMode(int client)
+{
+ // If GOKZ core isn't loaded, then apply mode at all times
+ return !gB_GOKZCore || GOKZ_GetCoreOption(client, Option_Mode) == Mode_SimpleKZ;
+}
+
+void ResetClient(int client)
+{
+ KZPlayer player = KZPlayer(client);
+ ResetVelMod(player);
+}
+
+void HookEvents()
+{
+ GameData gameData = LoadGameConfigFile("movementapi.games");
+ if (gameData == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find movementapi.games config");
+ }
+
+ int offset = gameData.GetOffset("GetPlayerMaxSpeed");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get GetPlayerMaxSpeed offset");
+ }
+ gH_GetPlayerMaxSpeed = DHookCreate(offset, HookType_Entity, ReturnType_Float, ThisPointer_CBaseEntity, DHooks_OnGetPlayerMaxSpeed);
+
+ gH_AirAccelerate = DynamicDetour.FromConf(gameData, "CGameMovement::AirAccelerate");
+ if (gH_AirAccelerate == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find CGameMovement::AirAccelerate function signature");
+ }
+
+ if (!gH_AirAccelerate.Enable(Hook_Pre, DHooks_OnAirAccelerate_Pre))
+ {
+ SetFailState("Failed to enable detour on CGameMovement::AirAccelerate");
+ }
+
+ char buffer[16];
+ if (!gameData.GetKeyValue("CGameMovement::player", buffer, sizeof(buffer)))
+ {
+ SetFailState("Failed to get CGameMovement::player offset.");
+ }
+ gI_OffsetCGameMovement_player = StringToInt(buffer);
+
+ gameData = LoadGameConfigFile("gokz-core.games");
+ gH_CanUnduck = DynamicDetour.FromConf(gameData, "CCSGameMovement::CanUnduck");
+ if (gH_CanUnduck == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find CCSGameMovement::CanUnduck function signature");
+ }
+
+ if (!gH_CanUnduck.Enable(Hook_Pre, DHooks_OnCanUnduck_Pre))
+ {
+ SetFailState("Failed to enable detour on CCSGameMovement::CanUnduck");
+ }
+ delete gameData;
+}
+
+// =====[ CONVARS ]=====
+
+void CreateConVars()
+{
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i] = FindConVar(gC_ModeCVars[i]);
+ }
+}
+
+void TweakConVars()
+{
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i].FloatValue = gF_ModeCVarValues[i];
+ }
+}
+
+void ReplicateConVars(int client)
+{
+ /*
+ Replicate convars only when player changes mode in GOKZ
+ so that lagg isn't caused by other players using other
+ modes, and also as an optimisation.
+ */
+
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i].ReplicateToClient(client, FloatToStringEx(gF_ModeCVarValues[i]));
+ }
+}
+
+
+
+// =====[ VELOCITY MODIFIER ]=====
+
+void HookClientEvents(int client)
+{
+ DHookEntity(gH_GetPlayerMaxSpeed, true, client);
+ SDKHook(client, SDKHook_PreThinkPost, SDKHook_OnClientPreThink_Post);
+}
+
+void ResetVelMod(KZPlayer player)
+{
+ gF_PSBonusSpeed[player.ID] = 0.0;
+ gF_PSVelMod[player.ID] = 1.0;
+ gF_PSTurnRate[player.ID] = 0.0;
+}
+
+void CalcPrestrafeVelMod(KZPlayer player, const float angles[3])
+{
+ gI_PSTicksSinceIncrement[player.ID]++;
+
+ // Short circuit if speed is 0 (also avoids divide by 0 errors)
+ if (player.Speed < EPSILON)
+ {
+ ResetVelMod(player);
+ return;
+ }
+
+ // Current speed without bonus
+ float baseSpeed = FloatMin(SPEED_NORMAL, player.Speed / gF_PSVelMod[player.ID]);
+
+ float newBonusSpeed = gF_PSBonusSpeed[player.ID];
+
+ // If player is in mid air, decrement their velocity modifier
+ if (!player.OnGround)
+ {
+ newBonusSpeed -= PS_SPEED_DECREMENT_MIDAIR;
+ }
+ // If player is turning at the required speed, and has the correct button inputs, reward it
+ else if (player.Turning && ValidPrestrafeButtons(player))
+ {
+ // If player changes their prestrafe direction, reset it
+ if (player.TurningLeft && !gB_PSTurningLeft[player.ID]
+ || player.TurningRight && gB_PSTurningLeft[player.ID])
+ {
+ ResetVelMod(player);
+ newBonusSpeed = 0.0;
+ }
+
+ // Keep track of the direction of the turn
+ gB_PSTurningLeft[player.ID] = player.TurningLeft;
+
+ // Step one of calculating new turn rate
+ float newTurningRate = FloatAbs(CalcDeltaAngle(gF_OldAngles[player.ID][1], angles[1]));
+
+ // If no turning for just a few ticks, then forgive and calculate reward based on that no. of ticks
+ if (gI_PSTicksSinceIncrement[player.ID] <= PS_GRACE_TICKS)
+ {
+ // This turn occurred over multiple ticks, so scale appropriately
+ // Also cap turn rate at maximum reward turn rate
+ newTurningRate = FloatMin(PS_MAX_REWARD_TURN_RATE,
+ newTurningRate / gI_PSTicksSinceIncrement[player.ID]);
+
+ // Limit how fast turn rate can decrease (also scaled appropriately)
+ gF_PSTurnRate[player.ID] = FloatMax(newTurningRate,
+ gF_PSTurnRate[player.ID] - PS_MAX_TURN_RATE_DECREMENT * gI_PSTicksSinceIncrement[player.ID]);
+
+ newBonusSpeed += CalcPreRewardSpeed(gF_PSTurnRate[player.ID], baseSpeed) * gI_PSTicksSinceIncrement[player.ID];
+ }
+ else
+ {
+ // Cap turn rate at maximum reward turn rate
+ newTurningRate = FloatMin(PS_MAX_REWARD_TURN_RATE, newTurningRate);
+
+ // Limit how fast turn rate can decrease
+ gF_PSTurnRate[player.ID] = FloatMax(newTurningRate,
+ gF_PSTurnRate[player.ID] - PS_MAX_TURN_RATE_DECREMENT);
+
+ // This is normal turning behaviour
+ newBonusSpeed += CalcPreRewardSpeed(gF_PSTurnRate[player.ID], baseSpeed);
+ }
+
+ gI_PSTicksSinceIncrement[player.ID] = 0;
+ }
+ else if (gI_PSTicksSinceIncrement[player.ID] > PS_GRACE_TICKS)
+ {
+ // They definitely aren't turning, but limit how fast turn rate can decrease
+ gF_PSTurnRate[player.ID] = FloatMax(0.0,
+ gF_PSTurnRate[player.ID] - PS_MAX_TURN_RATE_DECREMENT);
+ }
+
+ if (newBonusSpeed < 0.0)
+ {
+ // Keep velocity modifier positive
+ newBonusSpeed = 0.0;
+ }
+ else
+ {
+ // Scale the bonus speed based on current base speed and turn rate
+ float baseSpeedScaleFactor = baseSpeed / SPEED_NORMAL; // Max 1.0
+ float turnRateScaleFactor = FloatMin(1.0, gF_PSTurnRate[player.ID] / PS_MAX_REWARD_TURN_RATE);
+ float scaledMaxBonusSpeed = PS_SPEED_MAX * baseSpeedScaleFactor * turnRateScaleFactor;
+ newBonusSpeed = FloatMin(newBonusSpeed, scaledMaxBonusSpeed);
+ }
+
+ gF_PSBonusSpeed[player.ID] = newBonusSpeed;
+ gF_PSVelMod[player.ID] = 1.0 + (newBonusSpeed / baseSpeed);
+}
+
+bool ValidPrestrafeButtons(KZPlayer player)
+{
+ bool forwardOrBack = player.Buttons & (IN_FORWARD | IN_BACK) && !(player.Buttons & IN_FORWARD && player.Buttons & IN_BACK);
+ bool leftOrRight = player.Buttons & (IN_MOVELEFT | IN_MOVERIGHT) && !(player.Buttons & IN_MOVELEFT && player.Buttons & IN_MOVERIGHT);
+ return forwardOrBack || leftOrRight;
+}
+
+float CalcPreRewardSpeed(float yawDiff, float baseSpeed)
+{
+ // Formula
+ float reward;
+ if (yawDiff >= PS_MAX_REWARD_TURN_RATE)
+ {
+ reward = PS_SPEED_INCREMENT;
+ }
+ else
+ {
+ reward = PS_SPEED_INCREMENT * (yawDiff / PS_MAX_REWARD_TURN_RATE);
+ }
+
+ return reward * baseSpeed / SPEED_NORMAL;
+}
+
+
+
+
+// =====[ JUMPING ]=====
+
+Action TweakJump(KZPlayer player, float origin[3], float velocity[3])
+{
+ // TakeoffCmdnum and TakeoffSpeed is not defined here because the player technically hasn't taken off yet.
+ int cmdsSinceLanding = gI_Cmdnum[player.ID] - player.LandingCmdNum;
+ gB_HitTweakedPerf[player.ID] = cmdsSinceLanding <= 1
+ || cmdsSinceLanding <= 3 && gI_Cmdnum[player.ID] - gI_LastJumpButtonCmdnum[player.ID] <= 3;
+
+ if (gB_HitTweakedPerf[player.ID])
+ {
+ if (cmdsSinceLanding <= 1)
+ {
+ NerfRealPerf(player, origin);
+ }
+
+ ApplyTweakedTakeoffSpeed(player, velocity);
+
+ if (cmdsSinceLanding > 1 || player.TakeoffSpeed > SPEED_NORMAL)
+ {
+ // Restore prestrafe lost due to briefly being on the ground
+ gF_PSVelMod[player.ID] = gF_PSVelModLanding[player.ID];
+ }
+ return Plugin_Changed;
+ }
+ return Plugin_Continue;
+}
+
+public Action Movement_OnJumpPost(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+ KZPlayer player = KZPlayer(client);
+ player.GOKZHitPerf = gB_HitTweakedPerf[player.ID];
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ return Plugin_Continue;
+}
+
+public void Movement_OnStopTouchGround(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+ KZPlayer player = KZPlayer(client);
+ player.GOKZHitPerf = gB_HitTweakedPerf[player.ID];
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+}
+
+void NerfRealPerf(KZPlayer player, float origin[3])
+{
+ // Not worth worrying about if player is already falling
+ // player.VerticalVelocity is not updated yet! Use processing velocity.
+ float velocity[3];
+ Movement_GetProcessingVelocity(player.ID, velocity);
+ if (velocity[2] < EPSILON)
+ {
+ return;
+ }
+
+ // Work out where the ground was when they bunnyhopped
+ float startPosition[3], endPosition[3], mins[3], maxs[3], groundOrigin[3];
+
+ startPosition = origin;
+
+ endPosition = startPosition;
+ endPosition[2] = endPosition[2] - 2.0; // Should be less than 2.0 units away
+
+ GetEntPropVector(player.ID, Prop_Send, "m_vecMins", mins);
+ GetEntPropVector(player.ID, Prop_Send, "m_vecMaxs", maxs);
+
+ Handle trace = TR_TraceHullFilterEx(
+ startPosition,
+ endPosition,
+ mins,
+ maxs,
+ MASK_PLAYERSOLID,
+ TraceEntityFilterPlayers,
+ player.ID);
+
+ // This is expected to always hit, previously this can fail upon jumpbugs.
+ if (TR_DidHit(trace))
+ {
+ TR_GetEndPosition(groundOrigin, trace);
+ origin[2] = groundOrigin[2];
+ }
+
+ delete trace;
+}
+
+void ApplyTweakedTakeoffSpeed(KZPlayer player, float velocity[3])
+{
+ // Note that resulting velocity has same direction as landing velocity, not current velocity
+ // because current velocity direction can change drastically in just one tick (eg. walls)
+ // and it doesnt make sense for the new velocity to push you in that direction.
+
+ float newVelocity[3], baseVelocity[3];
+ player.GetLandingVelocity(newVelocity);
+ player.GetBaseVelocity(baseVelocity);
+ SetVectorHorizontalLength(newVelocity, CalcTweakedTakeoffSpeed(player));
+ AddVectors(newVelocity, baseVelocity, newVelocity); // For backwards compatibility
+ velocity[0] = newVelocity[0];
+ velocity[1] = newVelocity[1];
+}
+
+// Takeoff speed assuming player has met the conditions to need tweaking
+float CalcTweakedTakeoffSpeed(KZPlayer player)
+{
+ // Formula
+ if (player.LandingSpeed > SPEED_NORMAL)
+ {
+ return FloatMin(player.LandingSpeed, (0.2 * player.LandingSpeed + 200) * gF_PSVelModLanding[player.ID]);
+ }
+ return player.LandingSpeed;
+}
+
+
+
+// =====[ SLOPEFIX ]=====
+
+// ORIGINAL AUTHORS : Mev & Blacky
+// URL : https://forums.alliedmods.net/showthread.php?p=2322788
+// NOTE : Modified by DanZay for this plugin
+
+Action SlopeFix(int client, float origin[3], float velocity[3])
+{
+ KZPlayer player = KZPlayer(client);
+ // Check if player landed on the ground
+ if (Movement_GetOnGround(client) && !gB_OldOnGround[client])
+ {
+ float vMins[] = {-16.0, -16.0, 0.0};
+ // Always use ducked hull as the real hull size isn't updated yet.
+ // Might cause slight issues in extremely rare scenarios.
+ float vMaxs[] = {16.0, 16.0, 54.0};
+
+ float vEndPos[3];
+ vEndPos[0] = origin[0];
+ vEndPos[1] = origin[1];
+ vEndPos[2] = origin[2] - gF_ModeCVarValues[ModeCVar_MaxVelocity];
+
+ // Set up and do tracehull to find out if the player landed on a slope
+ TR_TraceHullFilter(origin, vEndPos, vMins, vMaxs, MASK_PLAYERSOLID_BRUSHONLY, TraceRayDontHitSelf, client);
+
+ if (TR_DidHit())
+ {
+ // Gets the normal vector of the surface under the player
+ float vPlane[3], vLast[3];
+ player.GetLandingVelocity(vLast);
+ TR_GetPlaneNormal(null, vPlane);
+
+ // Make sure it's not flat ground and not a surf ramp (1.0 = flat ground, < 0.7 = surf ramp)
+ if (0.7 <= vPlane[2] < 1.0)
+ {
+ /*
+ Copy the ClipVelocity function from sdk2013
+ (https://mxr.alliedmods.net/hl2sdk-sdk2013/source/game/shared/gamemovement.cpp#3145)
+ With some minor changes to make it actually work
+ */
+
+ float fBackOff = GetVectorDotProduct(vLast, vPlane);
+
+ float change, vVel[3];
+ for (int i; i < 2; i++)
+ {
+ change = vPlane[i] * fBackOff;
+ vVel[i] = vLast[i] - change;
+ }
+
+ float fAdjust = GetVectorDotProduct(vVel, vPlane);
+ if (fAdjust < 0.0)
+ {
+ for (int i; i < 2; i++)
+ {
+ vVel[i] -= (vPlane[i] * fAdjust);
+ }
+ }
+
+ vVel[2] = 0.0;
+ vLast[2] = 0.0;
+
+ // Make sure the player is going down a ramp by checking if they actually will gain speed from the boost
+ if (GetVectorLength(vVel) > GetVectorLength(vLast))
+ {
+ CopyVector(vVel, velocity);
+ player.SetLandingVelocity(velocity);
+ return Plugin_Changed;
+ }
+ }
+ }
+ }
+ return Plugin_Continue;
+}
+
+public bool TraceRayDontHitSelf(int entity, int mask, any data)
+{
+ return entity != data && !(0 < entity <= MaxClients);
+}
+
+
+
+// =====[ OTHER ]=====
+
+void FixWaterBoost(KZPlayer player, int buttons)
+{
+ if (GetEntProp(player.ID, Prop_Send, "m_nWaterLevel") >= 2) // WL_Waist = 2
+ {
+ // If duck is being pressed and we're not already ducking or on ground
+ if (GetEntityFlags(player.ID) & (FL_DUCKING | FL_ONGROUND) == 0
+ && buttons & IN_DUCK && ~gI_OldButtons[player.ID] & IN_DUCK)
+ {
+ float newOrigin[3];
+ Movement_GetOrigin(player.ID, newOrigin);
+ newOrigin[2] += 9.0;
+
+ TR_TraceHullFilter(newOrigin, newOrigin, view_as<float>( { -16.0, -16.0, 0.0 } ), view_as<float>( { 16.0, 16.0, 54.0 } ), MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+ if (!TR_DidHit())
+ {
+ TeleportEntity(player.ID, newOrigin, NULL_VECTOR, NULL_VECTOR);
+ }
+ }
+ }
+}
+
+void FixDisplacementStuck(KZPlayer player)
+{
+ int flags = GetEntityFlags(player.ID);
+ bool unducked = ~flags & FL_DUCKING && gI_OldFlags[player.ID] & FL_DUCKING;
+
+ float standingMins[] = {-16.0, -16.0, 0.0};
+ float standingMaxs[] = {16.0, 16.0, 72.0};
+
+ if (unducked)
+ {
+ // check if we're stuck after unducking and if we're stuck then force duck
+ float origin[3];
+ Movement_GetOrigin(player.ID, origin);
+ TR_TraceHullFilter(origin, origin, standingMins, standingMaxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+
+ if (TR_DidHit())
+ {
+ player.SetVelocity(gF_OldVelocity[player.ID]);
+ SetEntProp(player.ID, Prop_Send, "m_bDucking", true);
+ }
+ }
+}
+
+void RemoveCrouchJumpBind(KZPlayer player, int &buttons)
+{
+ if (player.OnGround && buttons & IN_JUMP && !(gI_OldButtons[player.ID] & IN_JUMP) && !(gI_OldButtons[player.ID] & IN_DUCK))
+ {
+ buttons &= ~IN_DUCK;
+ }
+}
+
+void ReduceDuckSlowdown(KZPlayer player)
+{
+ /*
+ Duck speed is reduced by the game upon ducking or unducking.
+ The goal here is to accept that duck speed is reduced, but
+ stop it from being reduced further when spamming duck.
+
+ This is done by enforcing a minimum duck speed equivalent to
+ the value as if the player only ducked once. When not in not
+ in the middle of ducking, duck speed is reset to its normal
+ value in effort to reduce the number of times the minimum
+ duck speed is enforced. This should reduce noticeable lagg.
+ */
+
+ if (!GetEntProp(player.ID, Prop_Send, "m_bDucking")
+ && player.DuckSpeed < DUCK_SPEED_NORMAL - EPSILON)
+ {
+ player.DuckSpeed = DUCK_SPEED_NORMAL;
+ }
+ else if (player.DuckSpeed < DUCK_SPEED_MINIMUM - EPSILON)
+ {
+ player.DuckSpeed = DUCK_SPEED_MINIMUM;
+ }
+}
diff --git a/sourcemod/scripting/gokz-mode-vanilla.sp b/sourcemod/scripting/gokz-mode-vanilla.sp
new file mode 100644
index 0000000..aecc201
--- /dev/null
+++ b/sourcemod/scripting/gokz-mode-vanilla.sp
@@ -0,0 +1,291 @@
+#include <sourcemod>
+
+#include <sdkhooks>
+#include <sdktools>
+#include <dhooks>
+
+#include <movementapi>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/core>
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Mode - Vanilla",
+ author = "DanZay",
+ description = "Vanilla mode for GOKZ",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-mode-vanilla.txt"
+
+#define MODE_VERSION 17
+
+float gF_ModeCVarValues[MODECVAR_COUNT] =
+{
+ 5.5, // sv_accelerate
+ 1.0, // sv_accelerate_use_weapon_speed
+ 12.0, // sv_airaccelerate
+ 30.0, // sv_air_max_wishspeed
+ 0.0, // sv_enablebunnyhopping
+ 5.2, // sv_friction
+ 800.0, // sv_gravity
+ 301.993377, // sv_jump_impulse
+ 0.78, // sv_ladder_scale_speed
+ 1.0, // sv_ledge_mantle_helper
+ 320.0, // sv_maxspeed
+ 3500.0, // sv_maxvelocity
+ 0.080, // sv_staminajumpcost
+ 0.050, // sv_staminalandcost
+ 80.0, // sv_staminamax
+ 60.0, // sv_staminarecoveryrate
+ 0.7, // sv_standable_normal
+ 0.4, // sv_timebetweenducks
+ 0.7, // sv_walkable_normal
+ 10.0, // sv_wateraccelerate
+ 0.8, // sv_water_movespeed_multiplier
+ 0.0, // sv_water_swim_mode
+ 0.85, // sv_weapon_encumbrance_per_item
+ 0.0 // sv_weapon_encumbrance_scale
+};
+
+bool gB_GOKZCore;
+ConVar gCV_ModeCVar[MODECVAR_COUNT];
+bool gB_ProcessingMaxSpeed[MAXPLAYERS + 1];
+Handle gH_GetPlayerMaxSpeed;
+Handle gH_GetPlayerMaxSpeed_SDKCall;
+
+// =====[ PLUGIN EVENTS ]=====
+
+public void OnPluginStart()
+{
+ CreateConVars();
+ HookEvents();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ if (LibraryExists("gokz-core"))
+ {
+ gB_GOKZCore = true;
+ GOKZ_SetModeLoaded(Mode_Vanilla, true, MODE_VERSION);
+ }
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ OnClientPutInServer(client);
+ }
+ }
+}
+
+public void OnPluginEnd()
+{
+ if (gB_GOKZCore)
+ {
+ GOKZ_SetModeLoaded(Mode_Vanilla, false);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ else if (StrEqual(name, "gokz-core"))
+ {
+ gB_GOKZCore = true;
+ GOKZ_SetModeLoaded(Mode_Vanilla, true, MODE_VERSION);
+ }
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZCore = gB_GOKZCore && !StrEqual(name, "gokz-core");
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ if (IsValidClient(client))
+ {
+ HookClientEvents(client);
+ }
+ if (IsUsingMode(client))
+ {
+ ReplicateConVars(client);
+ }
+}
+
+void HookClientEvents(int client)
+{
+ DHookEntity(gH_GetPlayerMaxSpeed, true, client);
+ SDKHook(client, SDKHook_PreThinkPost, SDKHook_OnClientPreThink_Post);
+}
+
+public MRESReturn DHooks_OnGetPlayerMaxSpeed(int client, Handle hReturn)
+{
+ if (!IsUsingMode(client) || gB_ProcessingMaxSpeed[client])
+ {
+ return MRES_Ignored;
+ }
+ gB_ProcessingMaxSpeed[client] = true;
+ float maxSpeed = SDKCall(gH_GetPlayerMaxSpeed_SDKCall, client);
+ // Prevent players from running faster than 250u/s
+ if (maxSpeed > SPEED_NORMAL)
+ {
+ DHookSetReturn(hReturn, SPEED_NORMAL);
+ gB_ProcessingMaxSpeed[client] = false;
+ return MRES_Supercede;
+ }
+ gB_ProcessingMaxSpeed[client] = false;
+ return MRES_Ignored;
+}
+
+public void SDKHook_OnClientPreThink_Post(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+
+ // Don't tweak convars if GOKZ isn't running
+ if (gB_GOKZCore)
+ {
+ TweakConVars();
+ }
+}
+
+public Action Movement_OnJumpPost(int client)
+{
+ if (!IsUsingMode(client))
+ {
+ return Plugin_Continue;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore)
+ {
+ player.GOKZHitPerf = player.HitPerf;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+ return Plugin_Continue;
+}
+public void Movement_OnStopTouchGround(int client, bool jumped)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore)
+ {
+ player.GOKZHitPerf = player.HitPerf;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+}
+
+public void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype)
+{
+ if (!IsUsingMode(client))
+ {
+ return;
+ }
+
+ KZPlayer player = KZPlayer(client);
+ if (gB_GOKZCore && newMovetype == MOVETYPE_WALK)
+ {
+ player.GOKZHitPerf = false;
+ player.GOKZTakeoffSpeed = player.TakeoffSpeed;
+ }
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, gC_CoreOptionNames[Option_Mode]) && newValue == Mode_Vanilla)
+ {
+ ReplicateConVars(client);
+ }
+}
+
+
+
+// =====[ GENERAL ]=====
+
+bool IsUsingMode(int client)
+{
+ // If GOKZ core isn't loaded, then apply mode at all times
+ return !gB_GOKZCore || GOKZ_GetCoreOption(client, Option_Mode) == Mode_Vanilla;
+}
+
+void HookEvents()
+{
+ GameData gameData = LoadGameConfigFile("movementapi.games");
+ int offset = gameData.GetOffset("GetPlayerMaxSpeed");
+ if (offset == -1)
+ {
+ SetFailState("Failed to get GetPlayerMaxSpeed offset");
+ }
+ gH_GetPlayerMaxSpeed = DHookCreate(offset, HookType_Entity, ReturnType_Float, ThisPointer_CBaseEntity, DHooks_OnGetPlayerMaxSpeed);
+
+ StartPrepSDKCall(SDKCall_Player);
+ PrepSDKCall_SetFromConf(gameData, SDKConf_Virtual, "GetPlayerMaxSpeed");
+ PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_ByValue);
+ gH_GetPlayerMaxSpeed_SDKCall = EndPrepSDKCall();
+}
+
+
+// =====[ CONVARS ]=====
+
+void CreateConVars()
+{
+ for (int cvar = 0; cvar < MODECVAR_COUNT; cvar++)
+ {
+ gCV_ModeCVar[cvar] = FindConVar(gC_ModeCVars[cvar]);
+ }
+}
+
+void TweakConVars()
+{
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i].FloatValue = gF_ModeCVarValues[i];
+ }
+}
+
+void ReplicateConVars(int client)
+{
+ // Replicate convars only when player changes mode in GOKZ
+ // so that lagg isn't caused by other players using other
+ // modes, and also as an optimisation.
+
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+
+ for (int i = 0; i < MODECVAR_COUNT; i++)
+ {
+ gCV_ModeCVar[i].ReplicateToClient(client, FloatToStringEx(gF_ModeCVarValues[i]));
+ }
+}
diff --git a/sourcemod/scripting/gokz-momsurffix.sp b/sourcemod/scripting/gokz-momsurffix.sp
new file mode 100644
index 0000000..2738e90
--- /dev/null
+++ b/sourcemod/scripting/gokz-momsurffix.sp
@@ -0,0 +1,724 @@
+#include "sourcemod"
+#include "sdktools"
+#include "sdkhooks"
+#include "dhooks"
+
+#include <gokz/core>
+#include <gokz/momsurffix>
+
+#define SNAME "[gokz-momsurffix] "
+#define GAME_DATA_FILE "gokz-momsurffix.games"
+//#define DEBUG_PROFILE
+//#define DEBUG_MEMTEST
+
+public Plugin myinfo = {
+ name = "GOKZ Momsurffix",
+ author = "GAMMA CASE",
+ description = "Ported surf fix from momentum mod. Modified for GOKZ by GameChaos. Original source code: https://github.com/GAMMACASE/MomSurfFix",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define FLT_EPSILON 1.192092896e-07
+#define MAX_CLIP_PLANES 5
+
+#define ASM_PATCH_LEN 17
+#define ASM_START_OFFSET 100
+
+#define WALKABLE_PLANE_NORMAL 0.7
+
+enum OSType
+{
+ OSUnknown = -1,
+ OSWindows = 1,
+ OSLinux = 2
+};
+
+OSType gOSType;
+EngineVersion gEngineVersion;
+
+#define ASSERTUTILS_FAILSTATE_FUNC SetFailStateCustom
+#define MEMUTILS_PLUGINENDCALL
+#include "glib/memutils"
+#undef MEMUTILS_PLUGINENDCALL
+
+#include "momsurffix/utils.sp"
+#include "momsurffix/baseplayer.sp"
+#include "momsurffix/gametrace.sp"
+#include "momsurffix/gamemovement.sp"
+
+ConVar gBounce;
+
+float vec3_origin[3] = {0.0, 0.0, 0.0};
+bool gBasePlayerLoadedTooEarly;
+
+#if defined DEBUG_PROFILE
+#include "profiler"
+Profiler gProf;
+ArrayList gProfData;
+float gProfTime;
+
+void PROF_START()
+{
+ if(gProf)
+ gProf.Start();
+}
+
+void PROF_STOP(int idx)
+{
+ if(gProf)
+ {
+ gProf.Stop();
+ Prof_Check(idx);
+ }
+}
+
+#else
+#define PROF_START%1;
+#define PROF_STOP%1;
+#endif
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-momsurffix");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+#if defined DEBUG_MEMTEST
+ RegAdminCmd("sm_mom_dumpmempool", SM_Dumpmempool, ADMFLAG_ROOT, "Dumps active momory pool. Mainly for debugging.");
+#endif
+#if defined DEBUG_PROFILE
+ RegAdminCmd("sm_mom_prof", SM_Prof, ADMFLAG_ROOT, "Profiles performance of some expensive parts. Mainly for debugging.");
+#endif
+
+ gBounce = FindConVar("sv_bounce");
+ ASSERT_MSG(gBounce, "\"sv_bounce\" convar wasn't found!");
+
+ GameData gd = new GameData(GAME_DATA_FILE);
+ ASSERT_FINAL(gd);
+
+ ValidateGameAndOS(gd);
+
+ InitUtils(gd);
+ InitGameTrace(gd);
+ gBasePlayerLoadedTooEarly = InitBasePlayer(gd);
+ InitGameMovement(gd);
+
+ SetupDhooks(gd);
+
+ delete gd;
+}
+
+public void OnMapStart()
+{
+ if(gBasePlayerLoadedTooEarly)
+ {
+ GameData gd = new GameData(GAME_DATA_FILE);
+ LateInitBasePlayer(gd);
+ gBasePlayerLoadedTooEarly = false;
+ delete gd;
+ }
+}
+
+public void OnPluginEnd()
+{
+ CleanUpUtils();
+}
+
+#if defined DEBUG_MEMTEST
+public Action SM_Dumpmempool(int client, int args)
+{
+ DumpMemoryUsage();
+
+ return Plugin_Handled;
+}
+#endif
+
+#if defined DEBUG_PROFILE
+public Action SM_Prof(int client, int args)
+{
+ if(args < 1)
+ {
+ ReplyToCommand(client, SNAME..."Usage: sm_prof <seconds>");
+ return Plugin_Handled;
+ }
+
+ char buff[32];
+ GetCmdArg(1, buff, sizeof(buff));
+ gProfTime = StringToFloat(buff);
+
+ if(gProfTime <= 0.1)
+ {
+ ReplyToCommand(client, SNAME..."Time should be higher then 0.1 seconds.");
+ return Plugin_Handled;
+ }
+
+ gProfData = new ArrayList(3);
+ gProf = new Profiler();
+ CreateTimer(gProfTime, Prof_Check_Timer, client);
+
+ ReplyToCommand(client, SNAME..."Profiler started, awaiting %.2f seconds.", gProfTime);
+
+ return Plugin_Handled;
+}
+
+stock void Prof_Check(int idx)
+{
+ int idx2;
+ if(gProfData.Length - 1 < idx)
+ {
+ idx2 = gProfData.Push(gProf.Time);
+ gProfData.Set(idx2, 1, 1);
+ gProfData.Set(idx2, idx, 2);
+ }
+ else
+ {
+ idx2 = gProfData.FindValue(idx, 2);
+
+ gProfData.Set(idx2, view_as<float>(gProfData.Get(idx2)) + gProf.Time);
+ gProfData.Set(idx2, gProfData.Get(idx2, 1) + 1, 1);
+ }
+}
+
+public Action Prof_Check_Timer(Handle timer, int client)
+{
+ ReplyToCommand(client, SNAME..."Profiler finished:");
+ if(gProfData.Length == 0)
+ ReplyToCommand(client, SNAME..."There was no profiling data...");
+
+ for(int i = 0; i < gProfData.Length; i++)
+ ReplyToCommand(client, SNAME..."[%i] Avg time: %f | Calls: %i", i, view_as<float>(gProfData.Get(i)) / float(gProfData.Get(i, 1)), gProfData.Get(i, 1));
+
+ delete gProf;
+ delete gProfData;
+
+ return Plugin_Handled;
+}
+#endif
+
+void ValidateGameAndOS(GameData gd)
+{
+ gOSType = view_as<OSType>(gd.GetOffset("OSType"));
+ ASSERT_FINAL_MSG(gOSType != OSUnknown, "Failed to get OS type or you are trying to load it on unsupported OS!");
+
+ gEngineVersion = GetEngineVersion();
+ ASSERT_FINAL_MSG(gEngineVersion == Engine_CSS || gEngineVersion == Engine_CSGO, "Only CSGO and CSS are supported by this plugin!");
+}
+
+void SetupDhooks(GameData gd)
+{
+ Handle dhook = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Int, ThisPointer_Address);
+
+ DHookSetFromConf(dhook, gd, SDKConf_Signature, "CGameMovement::TryPlayerMove");
+ DHookAddParam(dhook, HookParamType_Int);
+ DHookAddParam(dhook, HookParamType_Int);
+
+ ASSERT(DHookEnableDetour(dhook, false, TryPlayerMove_Dhook));
+}
+
+public MRESReturn TryPlayerMove_Dhook(Address pThis, Handle hReturn, Handle hParams)
+{
+ Address pFirstDest = DHookGetParam(hParams, 1);
+ Address pFirstTrace = DHookGetParam(hParams, 2);
+
+ DHookSetReturn(hReturn, TryPlayerMove(view_as<CGameMovement>(pThis), view_as<Vector>(pFirstDest), view_as<CGameTrace>(pFirstTrace)));
+
+ return MRES_Supercede;
+}
+
+int TryPlayerMove(CGameMovement pThis, Vector pFirstDest, CGameTrace pFirstTrace)
+{
+ float original_velocity[3], primal_velocity[3], fixed_origin[3], valid_plane[3], new_velocity[3], end[3], dir[3];
+ float allFraction, d, time_left = GetGameFrameTime(), planes[MAX_CLIP_PLANES][3];
+ int bumpcount, blocked, numplanes, numbumps = 8, i, j, h;
+ bool stuck_on_ramp, has_valid_plane;
+ CGameTrace pm = CGameTrace();
+
+ Vector vecVelocity = pThis.mv.m_vecVelocity;
+ vecVelocity.ToArray(original_velocity);
+ vecVelocity.ToArray(primal_velocity);
+ Vector vecAbsOrigin = pThis.mv.m_vecAbsOrigin;
+ vecAbsOrigin.ToArray(fixed_origin);
+
+ Vector plane_normal;
+ static Vector alloced_vector, alloced_vector2;
+
+ if(alloced_vector.Address == Address_Null)
+ alloced_vector = Vector();
+
+ if(alloced_vector2.Address == Address_Null)
+ alloced_vector2 = Vector();
+
+ const float rampInitialRetraceLength = 0.03125;
+ for(bumpcount = 0; bumpcount < numbumps; bumpcount++)
+ {
+ if(vecVelocity.LengthSqr() == 0.0)
+ break;
+
+ if(stuck_on_ramp)
+ {
+ if(!has_valid_plane)
+ {
+ plane_normal = pm.plane.normal;
+ if(!CloseEnough(VectorToArray(plane_normal), view_as<float>({0.0, 0.0, 0.0})) &&
+ !IsEqual(valid_plane, VectorToArray(plane_normal)))
+ {
+ plane_normal.ToArray(valid_plane);
+ has_valid_plane = true;
+ }
+ else
+ {
+ for(i = numplanes; i-- > 0;)
+ {
+ if(!CloseEnough(planes[i], view_as<float>({0.0, 0.0, 0.0})) &&
+ FloatAbs(planes[i][0]) <= 1.0 && FloatAbs(planes[i][1]) <= 1.0 && FloatAbs(planes[i][2]) <= 1.0 &&
+ !IsEqual(valid_plane, planes[i]))
+ {
+ VectorCopy(planes[i], valid_plane);
+ has_valid_plane = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if(has_valid_plane)
+ {
+ alloced_vector.FromArray(valid_plane);
+ if(valid_plane[2] >= WALKABLE_PLANE_NORMAL && valid_plane[2] <= 1.0)
+ {
+ ClipVelocity(pThis, vecVelocity, alloced_vector, vecVelocity, 1.0);
+ vecVelocity.ToArray(original_velocity);
+ }
+ else
+ {
+ ClipVelocity(pThis, vecVelocity, alloced_vector, vecVelocity, 1.0 + gBounce.FloatValue * (1.0 - pThis.player.m_surfaceFriction));
+ vecVelocity.ToArray(original_velocity);
+ }
+ alloced_vector.ToArray(valid_plane);
+ }
+ //TODO: should be replaced with normal solution!! Currently hack to fix issue #1.
+ else if((vecVelocity.z < -6.25 || vecVelocity.z > 0.0))
+ {
+ //Quite heavy part of the code, should not be triggered much or else it'll impact performance by a lot!!!
+ float offsets[3];
+ offsets[0] = (float(bumpcount) * 2.0) * -rampInitialRetraceLength;
+ offsets[2] = (float(bumpcount) * 2.0) * rampInitialRetraceLength;
+ int valid_planes = 0;
+
+ VectorCopy(view_as<float>({0.0, 0.0, 0.0}), valid_plane);
+
+ float offset[3], offset_mins[3], offset_maxs[3], buff[3];
+ static Ray_t ray;
+
+ // Keep this variable allocated only once
+ // since ray.Init should take care of removing any left garbage values
+ if(ray.Address == Address_Null)
+ ray = Ray_t();
+
+ for(i = 0; i < 3; i++)
+ {
+ for(j = 0; j < 3; j++)
+ {
+ for(h = 0; h < 3; h++)
+ {
+ PROF_START();
+ offset[0] = offsets[i];
+ offset[1] = offsets[j];
+ offset[2] = offsets[h];
+
+ offset_mins = offset;
+ ScaleVector(offset_mins, 0.5);
+ offset_maxs = offset;
+ ScaleVector(offset_maxs, 0.5);
+
+ if(offset[0] > 0.0)
+ offset_mins[0] /= 2.0;
+ if(offset[1] > 0.0)
+ offset_mins[1] /= 2.0;
+ if(offset[2] > 0.0)
+ offset_mins[2] /= 2.0;
+
+ if(offset[0] < 0.0)
+ offset_maxs[0] /= 2.0;
+ if(offset[1] < 0.0)
+ offset_maxs[1] /= 2.0;
+ if(offset[2] < 0.0)
+ offset_maxs[2] /= 2.0;
+ PROF_STOP(0);
+
+ PROF_START();
+ AddVectors(fixed_origin, offset, buff);
+ SubtractVectors(end, offset, offset);
+ if(gEngineVersion == Engine_CSGO)
+ {
+ SubtractVectors(VectorToArray(GetPlayerMins(pThis)), offset_mins, offset_mins);
+ AddVectors(VectorToArray(GetPlayerMaxs(pThis)), offset_maxs, offset_maxs);
+ }
+ else
+ {
+ SubtractVectors(VectorToArray(GetPlayerMinsCSS(pThis, alloced_vector)), offset_mins, offset_mins);
+ AddVectors(VectorToArray(GetPlayerMaxsCSS(pThis, alloced_vector2)), offset_maxs, offset_maxs);
+ }
+ PROF_STOP(1);
+
+ PROF_START();
+ ray.Init(buff, offset, offset_mins, offset_maxs);
+ PROF_STOP(2);
+
+ PROF_START();
+ UTIL_TraceRay(ray, MASK_PLAYERSOLID, pThis, COLLISION_GROUP_PLAYER_MOVEMENT, pm);
+ PROF_STOP(3);
+
+ PROF_START();
+ plane_normal = pm.plane.normal;
+
+ if(FloatAbs(plane_normal.x) <= 1.0 && FloatAbs(plane_normal.y) <= 1.0 &&
+ FloatAbs(plane_normal.z) <= 1.0 && pm.fraction > 0.0 && pm.fraction < 1.0 && !pm.startsolid)
+ {
+ valid_planes++;
+ AddVectors(valid_plane, VectorToArray(plane_normal), valid_plane);
+ }
+ PROF_STOP(4);
+ }
+ }
+ }
+
+ if(valid_planes != 0 && !CloseEnough(valid_plane, view_as<float>({0.0, 0.0, 0.0})))
+ {
+ has_valid_plane = true;
+ NormalizeVector(valid_plane, valid_plane);
+ continue;
+ }
+ }
+
+ if(has_valid_plane)
+ {
+ VectorMA(fixed_origin, rampInitialRetraceLength, valid_plane, fixed_origin);
+ }
+ else
+ {
+ stuck_on_ramp = false;
+ continue;
+ }
+ }
+
+ VectorMA(fixed_origin, time_left, VectorToArray(vecVelocity), end);
+
+ if(pFirstDest.Address != Address_Null && IsEqual(end, VectorToArray(pFirstDest)))
+ {
+ pm.Free();
+ pm = pFirstTrace;
+ }
+ else
+ {
+ alloced_vector2.FromArray(end);
+
+ if(stuck_on_ramp && has_valid_plane)
+ {
+ alloced_vector.FromArray(fixed_origin);
+ TracePlayerBBox(pThis, alloced_vector, alloced_vector2, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, pm);
+ pm.plane.normal.FromArray(valid_plane);
+ }
+ else
+ {
+ TracePlayerBBox(pThis, vecAbsOrigin, alloced_vector2, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, pm);
+ }
+ }
+
+ if(bumpcount > 0 && pThis.player.m_hGroundEntity == view_as<Address>(-1) && !IsValidMovementTrace(pThis, pm))
+ {
+ has_valid_plane = false;
+ stuck_on_ramp = true;
+ continue;
+ }
+
+ if(pm.fraction > 0.0)
+ {
+ if((bumpcount == 0 || pThis.player.m_hGroundEntity != view_as<Address>(-1)) && numbumps > 0 && pm.fraction == 1.0)
+ {
+ CGameTrace stuck = CGameTrace();
+ TracePlayerBBox(pThis, pm.endpos, pm.endpos, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, stuck);
+
+ if((stuck.startsolid || stuck.fraction != 1.0) && bumpcount == 0)
+ {
+ has_valid_plane = false;
+ stuck_on_ramp = true;
+
+ stuck.Free();
+ continue;
+ }
+ else if(stuck.startsolid || stuck.fraction != 1.0)
+ {
+ vecVelocity.FromArray(vec3_origin);
+
+ stuck.Free();
+ break;
+ }
+
+ stuck.Free();
+ }
+
+ has_valid_plane = false;
+ stuck_on_ramp = false;
+
+ vecVelocity.ToArray(original_velocity);
+ vecAbsOrigin.FromArray(VectorToArray(pm.endpos));
+ vecAbsOrigin.ToArray(fixed_origin);
+ allFraction += pm.fraction;
+ numplanes = 0;
+ }
+
+ if(CloseEnoughFloat(pm.fraction, 1.0))
+ break;
+
+ MoveHelper().AddToTouched(pm, vecVelocity);
+
+ if(pm.plane.normal.z >= WALKABLE_PLANE_NORMAL)
+ blocked |= 1;
+
+ if(CloseEnoughFloat(pm.plane.normal.z, 0.0))
+ blocked |= 2;
+
+ time_left -= time_left * pm.fraction;
+
+ if(numplanes >= MAX_CLIP_PLANES)
+ {
+ vecVelocity.FromArray(vec3_origin);
+ break;
+ }
+
+ pm.plane.normal.ToArray(planes[numplanes]);
+ numplanes++;
+
+ if(numplanes == 1 && pThis.player.m_MoveType == MOVETYPE_WALK && pThis.player.m_hGroundEntity != view_as<Address>(-1))
+ {
+ Vector vec1 = Vector();
+ PROF_START();
+ if(planes[0][2] >= WALKABLE_PLANE_NORMAL)
+ {
+ vec1.FromArray(original_velocity);
+ alloced_vector2.FromArray(planes[0]);
+ alloced_vector.FromArray(new_velocity);
+ ClipVelocity(pThis, vec1, alloced_vector2, alloced_vector, 1.0);
+ alloced_vector.ToArray(original_velocity);
+ alloced_vector.ToArray(new_velocity);
+ }
+ else
+ {
+ vec1.FromArray(original_velocity);
+ alloced_vector2.FromArray(planes[0]);
+ alloced_vector.FromArray(new_velocity);
+ ClipVelocity(pThis, vec1, alloced_vector2, alloced_vector, 1.0 + gBounce.FloatValue * (1.0 - pThis.player.m_surfaceFriction));
+ alloced_vector.ToArray(new_velocity);
+ }
+ PROF_STOP(5);
+
+ vecVelocity.FromArray(new_velocity);
+ VectorCopy(new_velocity, original_velocity);
+
+ vec1.Free();
+ }
+ else
+ {
+ for(i = 0; i < numplanes; i++)
+ {
+ alloced_vector2.FromArray(original_velocity);
+ alloced_vector.FromArray(planes[i]);
+ ClipVelocity(pThis, alloced_vector2, alloced_vector, vecVelocity, 1.0);
+ alloced_vector.ToArray(planes[i]);
+
+ for(j = 0; j < numplanes; j++)
+ if(j != i)
+ if(vecVelocity.Dot(planes[j]) < 0.0)
+ break;
+
+ if(j == numplanes)
+ break;
+ }
+
+ if(i != numplanes)
+ {
+
+ }
+ else
+ {
+ if(numplanes != 2)
+ {
+ vecVelocity.FromArray(vec3_origin);
+ break;
+ }
+
+ // Fun fact time: these next five lines of code fix (vertical) rampbug
+ if(CloseEnough(planes[0], planes[1]))
+ {
+ // Why did the above return true? Well, when surfing, you can "clip" into the
+ // ramp, due to the ramp not pushing you away enough, and when that happens,
+ // a surfer cries. So the game thinks the surfer is clipping along two of the exact
+ // same planes. So what we do here is take the surfer's original velocity,
+ // and add the along the normal of the surf ramp they're currently riding down,
+ // essentially pushing them away from the ramp.
+
+ // NOTE: the following comment is here for context:
+ // NOTE: Technically the 20.0 here can be 2.0, but that causes "jitters" sometimes, so I found
+ // 20 to be pretty safe and smooth. If it causes any unforeseen consequences, tweak it!
+ VectorMA(original_velocity, 2.0, planes[0], new_velocity);
+ vecVelocity.x = new_velocity[0];
+ vecVelocity.y = new_velocity[1];
+ // Note: We don't want the player to gain any Z boost/reduce from this, gravity should be the
+ // only force working in the Z direction!
+
+ // Lastly, let's get out of here before the following lines of code make the surfer lose speed.
+
+ break;
+ }
+
+ GetVectorCrossProduct(planes[0], planes[1], dir);
+ NormalizeVector(dir, dir);
+
+ d = vecVelocity.Dot(dir);
+
+ ScaleVector(dir, d);
+ vecVelocity.FromArray(dir);
+ }
+
+ d = vecVelocity.Dot(primal_velocity);
+ if(d <= 0.0)
+ {
+ vecVelocity.FromArray(vec3_origin);
+ break;
+ }
+ }
+ }
+
+ if(CloseEnoughFloat(allFraction, 0.0))
+ vecVelocity.FromArray(vec3_origin);
+
+ pm.Free();
+ return blocked;
+}
+
+stock void VectorMA(float start[3], float scale, float dir[3], float dest[3])
+{
+ dest[0] = start[0] + dir[0] * scale;
+ dest[1] = start[1] + dir[1] * scale;
+ dest[2] = start[2] + dir[2] * scale;
+}
+
+stock void VectorCopy(float from[3], float to[3])
+{
+ to[0] = from[0];
+ to[1] = from[1];
+ to[2] = from[2];
+}
+
+stock float[] VectorToArray(Vector vec)
+{
+ float ret[3];
+ vec.ToArray(ret);
+ return ret;
+}
+
+stock bool IsEqual(float a[3], float b[3])
+{
+ return a[0] == b[0] && a[1] == b[1] && a[2] == b[2];
+}
+
+stock bool CloseEnough(float a[3], float b[3], float eps = FLT_EPSILON)
+{
+ return FloatAbs(a[0] - b[0]) <= eps &&
+ FloatAbs(a[1] - b[1]) <= eps &&
+ FloatAbs(a[2] - b[2]) <= eps;
+}
+
+stock bool CloseEnoughFloat(float a, float b, float eps = FLT_EPSILON)
+{
+ return FloatAbs(a - b) <= eps;
+}
+
+public void SetFailStateCustom(const char[] fmt, any ...)
+{
+ char buff[512];
+ VFormat(buff, sizeof(buff), fmt, 2);
+
+ CleanUpUtils();
+
+ char ostype[32];
+ switch(gOSType)
+ {
+ case OSLinux: ostype = "LIN";
+ case OSWindows: ostype = "WIN";
+ default: ostype = "UNK";
+ }
+
+ SetFailState("[%s | %i] %s", ostype, gEngineVersion, buff);
+}
+
+// 0-2 are axial planes
+#define PLANE_X 0
+#define PLANE_Y 1
+#define PLANE_Z 2
+
+// 3-5 are non-axial planes snapped to the nearest
+#define PLANE_ANYX 3
+#define PLANE_ANYY 4
+#define PLANE_ANYZ 5
+
+stock bool IsValidMovementTrace(CGameMovement pThis, CGameTrace tr)
+{
+ if(tr.allsolid || tr.startsolid)
+ return false;
+
+ // This fixes pixelsurfs in a kind of scuffed way
+ Vector plane_normal = tr.plane.normal;
+ if(CloseEnoughFloat(tr.fraction, 0.0)
+ && tr.plane.type >= PLANE_Z // axially aligned vertical planes (not floors) can be pixelsurfs!
+ && plane_normal.z < WALKABLE_PLANE_NORMAL) // if plane isn't walkable
+ {
+ return false;
+ }
+
+ if(FloatAbs(plane_normal.x) > 1.0 || FloatAbs(plane_normal.y) > 1.0 || FloatAbs(plane_normal.z) > 1.0)
+ return false;
+
+ CGameTrace stuck = CGameTrace();
+
+ TracePlayerBBox(pThis, tr.endpos, tr.endpos, MASK_PLAYERSOLID, COLLISION_GROUP_PLAYER_MOVEMENT, stuck);
+ if(stuck.startsolid || !CloseEnoughFloat(stuck.fraction, 1.0))
+ {
+ stuck.Free();
+ return false;
+ }
+
+ stuck.Free();
+ return true;
+}
+
+stock void UTIL_TraceRay(Ray_t ray, int mask, CGameMovement gm, int collisionGroup, CGameTrace trace)
+{
+ if(gEngineVersion == Engine_CSGO)
+ {
+ CTraceFilterSimple filter = LockTraceFilter(gm, collisionGroup);
+
+ gm.m_nTraceCount++;
+ ITraceListData tracelist = gm.m_pTraceListData;
+
+ if(tracelist.Address != Address_Null && tracelist.CanTraceRay(ray))
+ TraceRayAgainstLeafAndEntityList(ray, tracelist, mask, filter, trace);
+ else
+ TraceRay(ray, mask, filter, trace);
+
+ UnlockTraceFilter(gm, filter);
+ }
+ else if(gEngineVersion == Engine_CSS)
+ {
+ CTraceFilterSimple filter = CTraceFilterSimple();
+ filter.Init(LookupEntity(gm.mv.m_nPlayerHandle), collisionGroup);
+
+ TraceRay(ray, mask, filter, trace);
+
+ filter.Free();
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-paint.sp b/sourcemod/scripting/gokz-paint.sp
new file mode 100644
index 0000000..3de93a7
--- /dev/null
+++ b/sourcemod/scripting/gokz-paint.sp
@@ -0,0 +1,410 @@
+#include <sourcemod>
+
+#include <gokz/core>
+#include <gokz/paint>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+// Credit to SlidyBat for a large part of the painting code (https://forums.alliedmods.net/showthread.php?p=2541664)
+// Credit to Cabbage McGravel of the MomentumMod team for making the textures
+
+public Plugin myinfo =
+{
+ name = "GOKZ Paint",
+ author = "zealain",
+ description = "Provides client sided paint for visibility",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-paint.txt"
+
+char gC_PaintColors[][32] =
+{
+ "paint_red",
+ "paint_white",
+ "paint_black",
+ "paint_blue",
+ "paint_brown",
+ "paint_green",
+ "paint_yellow",
+ "paint_purple"
+};
+
+char gC_PaintSizePostfix[][8] =
+{
+ "_small",
+ "_med",
+ "_large"
+};
+
+int gI_Decals[sizeof(gC_PaintColors)][sizeof(gC_PaintSizePostfix)];
+float gF_LastPaintPos[MAXPLAYERS + 1][3];
+bool gB_IsPainting[MAXPLAYERS + 1];
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatPaint;
+TopMenuObject gTMO_ItemsPaint[PAINTOPTION_COUNT];
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-paint");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-paint.phrases");
+
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ OnOptionsMenuCreated_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+public void OnMapStart()
+{
+ char buffer[PLATFORM_MAX_PATH];
+
+ AddFileToDownloadsTable("materials/gokz/paint/paint_decal.vtf");
+ for (int color = 0; color < sizeof(gC_PaintColors); color++)
+ {
+ for (int size = 0; size < sizeof(gC_PaintSizePostfix); size++)
+ {
+ Format(buffer, sizeof(buffer), "gokz/paint/%s%s.vmt", gC_PaintColors[color], gC_PaintSizePostfix[size]);
+ gI_Decals[color][size] = PrecachePaint(buffer);
+ }
+ }
+
+ CreateTimer(0.1, Timer_Paint, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE);
+}
+
+
+
+// =====[ PAINT ]=====
+
+void Paint(int client)
+{
+ if (!IsValidClient(client) ||
+ IsFakeClient(client))
+ {
+ return;
+ }
+
+ float position[3];
+ bool hit = GetPlayerEyeViewPoint(client, position);
+
+ if (!hit || GetVectorDistance(position, gF_LastPaintPos[client], true) < MIN_PAINT_SPACING)
+ {
+ return;
+ }
+
+ int paint = GOKZ_GetOption(client, gC_PaintOptionNames[PaintOption_Color]);
+ int size = GOKZ_GetOption(client, gC_PaintOptionNames[PaintOption_Size]);
+
+ TE_SetupWorldDecal(position, gI_Decals[paint][size]);
+ TE_SendToClient(client);
+
+ gF_LastPaintPos[client] = position;
+}
+
+public Action Timer_Paint(Handle timer)
+{
+ for (int client = 1; client <= MAXPLAYERS; client++)
+ {
+ if (gB_IsPainting[client])
+ {
+ Paint(client);
+ }
+ }
+ return Plugin_Continue;
+}
+
+void TE_SetupWorldDecal(const float origin[3], int index)
+{
+ TE_Start("World Decal");
+ TE_WriteVector("m_vecOrigin", origin);
+ TE_WriteNum("m_nIndex", index);
+}
+
+int PrecachePaint(char[] filename)
+{
+ char path[PLATFORM_MAX_PATH];
+ Format(path, sizeof(path), "materials/%s", filename);
+ AddFileToDownloadsTable(path);
+
+ return PrecacheDecal(filename, true);
+}
+
+bool GetPlayerEyeViewPoint(int client, float position[3])
+{
+ float angles[3];
+ GetClientEyeAngles(client, angles);
+
+ float origin[3];
+ GetClientEyePosition(client, origin);
+
+ Handle trace = TR_TraceRayFilterEx(origin, angles, MASK_PLAYERSOLID, RayType_Infinite, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ TR_GetEndPosition(position, trace);
+ delete trace;
+ return true;
+ }
+ delete trace;
+ return false;
+}
+
+
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void RegisterOptions()
+{
+ for (PaintOption option; option < PAINTOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_PaintOptionNames[option], gC_PaintOptionDescriptions[option],
+ OptionType_Int, gI_PaintOptionDefaults[option], 0, gI_PaintOptionCounts[option] - 1);
+ }
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu && gTMO_CatPaint != INVALID_TOPMENUOBJECT)
+ {
+ return;
+ }
+
+ gTMO_CatPaint = topMenu.AddCategory(PAINT_OPTION_CATEGORY, TopMenuHandler_Categories);
+}
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ // Make sure category exists
+ if (gTMO_CatPaint == INVALID_TOPMENUOBJECT)
+ {
+ GOKZ_OnOptionsMenuCreated(topMenu);
+ }
+
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+
+ // Add gokz-paint option items
+ for (int option = 0; option < view_as<int>(PAINTOPTION_COUNT); option++)
+ {
+ gTMO_ItemsPaint[option] = gTM_Options.AddItem(gC_PaintOptionNames[option], TopMenuHandler_Paint, gTMO_CatPaint);
+ }
+}
+
+void DisplayPaintOptionsMenu(int client)
+{
+ gTM_Options.DisplayCategory(gTMO_CatPaint, client);
+}
+
+public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == gTMO_CatPaint)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - Paint", param);
+ }
+ }
+}
+
+public void TopMenuHandler_Paint(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ PaintOption option = PAINTOPTION_INVALID;
+ for (int i = 0; i < view_as<int>(PAINTOPTION_COUNT); i++)
+ {
+ if (topobj_id == gTMO_ItemsPaint[i])
+ {
+ option = view_as<PaintOption>(i);
+ break;
+ }
+ }
+
+ if (option == PAINTOPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case PaintOption_Color:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_PaintOptionPhrases[option], param,
+ gC_PaintColorPhrases[GOKZ_GetOption(param, gC_PaintOptionNames[option])], param);
+ }
+ case PaintOption_Size:
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_PaintOptionPhrases[option], param,
+ gC_PaintSizePhrases[GOKZ_GetOption(param, gC_PaintOptionNames[option])], param);
+ }
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ if (option == PaintOption_Color)
+ {
+ DisplayColorMenu(param);
+ }
+ else
+ {
+ GOKZ_CycleOption(param, gC_PaintOptionNames[option]);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+ }
+}
+
+void DisplayColorMenu(int client)
+{
+ char buffer[32];
+
+ Menu menu = new Menu(MenuHandler_PaintColor);
+ menu.ExitButton = true;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Paint Color Menu - Title", client);
+
+ for (int i = 0; i < PAINTCOLOR_COUNT; i++)
+ {
+ FormatEx(buffer, sizeof(buffer), "%T", gC_PaintColorPhrases[i], client);
+ menu.AddItem(gC_PaintColors[i], buffer);
+ }
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+int MenuHandler_PaintColor(Menu menu, MenuAction action, int param1, int param2)
+{
+ switch (action)
+ {
+ case MenuAction_Select:
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+
+ for (int i = 0; i < PAINTCOLOR_COUNT; i++)
+ {
+ if (StrEqual(gC_PaintColors[i], item))
+ {
+ GOKZ_SetOption(param1, gC_PaintOptionNames[PaintOption_Color], i);
+ DisplayPaintOptionsMenu(param1);
+ return 0;
+ }
+ }
+ }
+
+ case MenuAction_Cancel:
+ {
+ if (param2 == MenuCancel_ExitBack)
+ {
+ DisplayPaintOptionsMenu(param1);
+ }
+ }
+
+ case MenuAction_End:
+ {
+ delete menu;
+ }
+ }
+
+ return 0;
+}
+
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("+paint", CommandPaintStart, "[KZ] Start painting.");
+ RegConsoleCmd("-paint", CommandPaintEnd, "[KZ] Stop painting.");
+ RegConsoleCmd("sm_paint", CommandPaint, "[KZ] Place a paint.");
+ RegConsoleCmd("sm_paintoptions", CommandPaintOptions, "[KZ] Open the paint options.");
+}
+
+public Action CommandPaintStart(int client, int args)
+{
+ gB_IsPainting[client] = true;
+ return Plugin_Handled;
+}
+
+public Action CommandPaintEnd(int client, int args)
+{
+ gB_IsPainting[client] = false;
+ return Plugin_Handled;
+}
+
+public Action CommandPaint(int client, int args)
+{
+ Paint(client);
+ return Plugin_Handled;
+}
+
+public Action CommandPaintOptions(int client, int args)
+{
+ DisplayPaintOptionsMenu(client);
+ return Plugin_Handled;
+}
diff --git a/sourcemod/scripting/gokz-pistol.sp b/sourcemod/scripting/gokz-pistol.sp
new file mode 100644
index 0000000..53f79d9
--- /dev/null
+++ b/sourcemod/scripting/gokz-pistol.sp
@@ -0,0 +1,303 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdktools>
+
+#include <gokz/core>
+#include <gokz/pistol>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Pistol",
+ author = "DanZay",
+ description = "Allows players to pick a pistol to KZ with",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-pistols.txt"
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatGeneral;
+TopMenuObject gTMO_ItemPistol;
+bool gB_CameFromOptionsMenu[MAXPLAYERS + 1];
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-pistol");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-pistol.phrases");
+
+ HookEvents();
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ UpdatePistol(client);
+ }
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, PISTOL_OPTION_NAME))
+ {
+ UpdatePistol(client);
+ }
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+
+
+// =====[ GENERAL ]=====
+
+void HookEvents()
+{
+ HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post);
+}
+
+
+
+// =====[ PISTOL ]=====
+
+void UpdatePistol(int client)
+{
+ GivePistol(client, GOKZ_GetOption(client, PISTOL_OPTION_NAME));
+}
+
+void GivePistol(int client, int pistol)
+{
+ if (!IsClientInGame(client) || !IsPlayerAlive(client)
+ || GetClientTeam(client) == CS_TEAM_NONE)
+ {
+ return;
+ }
+
+ int playerTeam = GetClientTeam(client);
+ bool switchedTeams = false;
+
+ // Switch teams to the side that buys that gun so that gun skins load
+ if (gI_PistolTeams[pistol] == CS_TEAM_CT && playerTeam != CS_TEAM_CT)
+ {
+ CS_SwitchTeam(client, CS_TEAM_CT);
+ switchedTeams = true;
+ }
+ else if (gI_PistolTeams[pistol] == CS_TEAM_T && playerTeam != CS_TEAM_T)
+ {
+ CS_SwitchTeam(client, CS_TEAM_T);
+ switchedTeams = true;
+ }
+
+ // Give the player this pistol (or remove it)
+ int currentPistol = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY);
+ if (currentPistol != -1)
+ {
+ RemovePlayerItem(client, currentPistol);
+ }
+
+ if (pistol == Pistol_Disabled)
+ {
+ // Force switch to knife to avoid weird behaviour
+ // Doesn't use EquipPlayerWeapon because server hangs when player spawns
+ FakeClientCommand(client, "use weapon_knife");
+ }
+ else
+ {
+ GivePlayerItem(client, gC_PistolClassNames[pistol]);
+ }
+
+ // Go back to original team
+ if (switchedTeams)
+ {
+ CS_SwitchTeam(client, playerTeam);
+ }
+}
+
+
+
+// =====[ PISTOL MENU ]=====
+
+void DisplayPistolMenu(int client, int atItem = 0, bool fromOptionsMenu = false)
+{
+ Menu menu = new Menu(MenuHandler_Pistol);
+ menu.SetTitle("%T", "Pistol Menu - Title", client);
+ PistolMenuAddItems(client, menu);
+ menu.DisplayAt(client, atItem, MENU_TIME_FOREVER);
+
+ gB_CameFromOptionsMenu[client] = fromOptionsMenu;
+}
+
+public int MenuHandler_Pistol(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ GOKZ_SetOption(param1, PISTOL_OPTION_NAME, param2);
+ DisplayPistolMenu(param1, param2 / 6 * 6, gB_CameFromOptionsMenu[param1]); // Re-display menu at same spot
+ }
+ else if (action == MenuAction_Cancel && gB_CameFromOptionsMenu[param1])
+ {
+ gTM_Options.Display(param1, TopMenuPosition_LastCategory);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void PistolMenuAddItems(int client, Menu menu)
+{
+ int selectedPistol = GOKZ_GetOption(client, PISTOL_OPTION_NAME);
+ char display[32];
+
+ for (int pistol = 0; pistol < PISTOL_COUNT; pistol++)
+ {
+ if (pistol == Pistol_Disabled)
+ {
+ FormatEx(display, sizeof(display), "%T", "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%s", gC_PistolNames[pistol]);
+ }
+
+ // Add asterisk to selected pistol
+ if (pistol == selectedPistol)
+ {
+ Format(display, sizeof(display), "%s*", display);
+ }
+
+ menu.AddItem("", display, ITEMDRAW_DEFAULT);
+ }
+}
+
+
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOption();
+}
+
+void RegisterOption()
+{
+ GOKZ_RegisterOption(PISTOL_OPTION_NAME, PISTOL_OPTION_DESCRIPTION,
+ OptionType_Int, Pistol_USPS, 0, PISTOL_COUNT - 1);
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+ gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY);
+ gTMO_ItemPistol = gTM_Options.AddItem(PISTOL_OPTION_NAME, TopMenuHandler_Pistol, gTMO_CatGeneral);
+}
+
+public void TopMenuHandler_Pistol(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (topobj_id != gTMO_ItemPistol)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ int pistol = GOKZ_GetOption(param, PISTOL_OPTION_NAME);
+ if (pistol == Pistol_Disabled)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - Pistol", param,
+ "Options Menu - Disabled", param);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %s",
+ "Options Menu - Pistol", param,
+ gC_PistolNames[pistol]);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ DisplayPistolMenu(param, _, true);
+ }
+}
+
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_pistol", CommandPistolMenu, "[KZ] Open the pistol selection menu.");
+}
+
+public Action CommandPistolMenu(int client, int args)
+{
+ DisplayPistolMenu(client);
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-playermodels.sp b/sourcemod/scripting/gokz-playermodels.sp
new file mode 100644
index 0000000..237b7df
--- /dev/null
+++ b/sourcemod/scripting/gokz-playermodels.sp
@@ -0,0 +1,198 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdktools>
+
+#include <gokz/core>
+
+#include <autoexecconfig>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Player Models",
+ author = "DanZay",
+ description = "Sets player's model upon spawning",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-playermodels.txt"
+#define PLAYER_MODEL_T "models/player/tm_leet_varianta.mdl"
+#define PLAYER_MODEL_CT "models/player/ctm_idf_variantc.mdl"
+#define PLAYER_MODEL_T_BOT "models/player/custom_player/legacy/tm_leet_varianta.mdl"
+#define PLAYER_MODEL_CT_BOT "models/player/custom_player/legacy/ctm_idf_variantc.mdl"
+ConVar gCV_gokz_player_models_alpha;
+ConVar gCV_sv_disable_immunity_alpha;
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-playermodels");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ CreateConVars();
+ HookEvents();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) // player_spawn post hook
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ if (IsValidClient(client))
+ {
+ // Can't use a timer here because it's not precise enough. We want exactly 2 ticks of delay!
+ // 2 ticks is the minimum amount of time after which gloves will work.
+ // The reason we need precision is because SetEntityModel momentarily resets the
+ // player hull to standing (or something along those lines), so when a player
+ // spawns/gets reset to a crouch tunnel where there's a trigger less than 18 units from the top
+ // of the ducked player hull, then they touch that trigger! SetEntityModel interferes with the
+ // fix for that (JoinTeam in gokz-core/misc calls TeleportPlayer in gokz.inc, which fixes that bug).
+ RequestFrame(RequestFrame_UpdatePlayerModel, GetClientUserId(client));
+ }
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ PrecachePlayerModels();
+}
+
+
+
+// =====[ GENERAL ]=====
+
+void HookEvents()
+{
+ HookEvent("player_spawn", OnPlayerSpawn, EventHookMode_Post);
+}
+
+
+
+// =====[ CONVARS ]=====
+
+void CreateConVars()
+{
+ AutoExecConfig_SetFile("gokz-playermodels", "sourcemod/gokz");
+ AutoExecConfig_SetCreateFile(true);
+
+ gCV_gokz_player_models_alpha = AutoExecConfig_CreateConVar("gokz_player_models_alpha", "65", "Amount of alpha (transparency) to set player models to.", _, true, 0.0, true, 255.0);
+ gCV_gokz_player_models_alpha.AddChangeHook(OnConVarChanged);
+
+ AutoExecConfig_ExecuteFile();
+ AutoExecConfig_CleanFile();
+
+ gCV_sv_disable_immunity_alpha = FindConVar("sv_disable_immunity_alpha");
+}
+
+public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ if (convar == gCV_gokz_player_models_alpha)
+ {
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && IsPlayerAlive(client))
+ {
+ UpdatePlayerModelAlpha(client);
+ }
+ }
+ }
+}
+
+
+
+// =====[ PLAYER MODELS ]=====
+
+public void RequestFrame_UpdatePlayerModel(int userid)
+{
+ RequestFrame(RequestFrame_UpdatePlayerModel2, userid);
+}
+
+public void RequestFrame_UpdatePlayerModel2(int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (!IsValidClient(client) || !IsPlayerAlive(client))
+ {
+ return;
+ }
+ // Bots are unaffected by the bobbing animation caused by the new models.
+ switch (GetClientTeam(client))
+ {
+ case CS_TEAM_T:
+ {
+ if (IsFakeClient(client))
+ {
+ SetEntityModel(client, PLAYER_MODEL_T_BOT);
+ }
+ else
+ {
+ SetEntityModel(client, PLAYER_MODEL_T);
+ }
+ }
+ case CS_TEAM_CT:
+ {
+ if (IsFakeClient(client))
+ {
+ SetEntityModel(client, PLAYER_MODEL_CT_BOT);
+ }
+ else
+ {
+ SetEntityModel(client, PLAYER_MODEL_CT);
+ }
+ }
+ }
+
+ UpdatePlayerModelAlpha(client);
+}
+
+void UpdatePlayerModelAlpha(int client)
+{
+ SetEntityRenderMode(client, RENDER_TRANSCOLOR);
+ SetEntityRenderColor(client, _, _, _, gCV_gokz_player_models_alpha.IntValue);
+}
+
+void PrecachePlayerModels()
+{
+ gCV_sv_disable_immunity_alpha.IntValue = 1; // Ensures player transparency works
+
+ PrecacheModel(PLAYER_MODEL_T, true);
+ PrecacheModel(PLAYER_MODEL_CT, true);
+ PrecacheModel(PLAYER_MODEL_T_BOT, true);
+ PrecacheModel(PLAYER_MODEL_CT_BOT, true);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-profile.sp b/sourcemod/scripting/gokz-profile.sp
new file mode 100644
index 0000000..9d92e61
--- /dev/null
+++ b/sourcemod/scripting/gokz-profile.sp
@@ -0,0 +1,396 @@
+#include <sourcemod>
+
+#include <cstrike>
+
+#include <gokz/core>
+#include <gokz/profile>
+#include <gokz/global>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+#include <gokz/chat>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Profile",
+ author = "zealain",
+ description = "Player profiles and ranks based on local and global data.",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-profile.txt"
+
+int gI_Rank[MAXPLAYERS + 1][MODE_COUNT];
+bool gB_Localranks;
+bool gB_Chat;
+
+#include "gokz-profile/options.sp"
+#include "gokz-profile/profile.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-profile");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("common.phrases");
+ LoadTranslations("gokz-profile.phrases");
+ CreateGlobalForwards();
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_Localranks = LibraryExists("gokz-localranks");
+ gB_Chat = LibraryExists("gokz-chat");
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ UpdateRank(client, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_Localranks = gB_Localranks || StrEqual(name, "gokz-localranks");
+ gB_Chat = gB_Chat || StrEqual(name, "gokz-chat");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_Localranks = gB_Localranks && !StrEqual(name, "gokz-localranks");
+ gB_Chat = gB_Chat && !StrEqual(name, "gokz-chat");
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public Action OnClientCommandKeyValues(int client, KeyValues kv)
+{
+ // Block clan tag changes - Credit: GoD-Tony (https://forums.alliedmods.net/showpost.php?p=2337679&postcount=6)
+ char cmd[16];
+ if (kv.GetSectionName(cmd, sizeof(cmd)) && StrEqual(cmd, "ClanTagChanged", false))
+ {
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+public void OnRebuildAdminCache(AdminCachePart part)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+ UpdateRank(client, mode);
+ }
+ }
+}
+
+public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ OnOptionsMenuCreated_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnOptionsLoaded(int client)
+{
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+ UpdateTags(client, gI_Rank[client][mode], mode);
+ }
+}
+
+public void OnClientConnected(int client)
+{
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ gI_Rank[client][mode] = 0;
+ }
+ Profile_OnClientConnected(client);
+}
+
+public void OnClientDisconnect(int client)
+{
+ Profile_OnClientDisconnect(client);
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ Option coreOption;
+ if (GOKZ_IsCoreOption(option, coreOption) && coreOption == Option_Mode)
+ {
+ UpdateRank(client, newValue);
+ }
+ else if (StrEqual(option, gC_ProfileOptionNames[ProfileOption_ShowRankChat], true)
+ || StrEqual(option, gC_ProfileOptionNames[ProfileOption_ShowRankClanTag], true)
+ || StrEqual(option, gC_ProfileOptionNames[ProfileOption_TagType], true))
+ {
+ UpdateRank(client, GOKZ_GetCoreOption(client, Option_Mode));
+ }
+}
+
+public void GOKZ_GL_OnPointsUpdated(int client, int mode)
+{
+ UpdateRank(client, mode);
+ Profile_OnPointsUpdated(client, mode);
+}
+
+public void UpdateRank(int client, int mode)
+{
+ if (!IsValidClient(client) || IsFakeClient(client))
+ {
+ return;
+ }
+
+ int tagType = GetAvailableTagTypeOrDefault(client);
+
+ if (tagType != ProfileTagType_Rank)
+ {
+ char clanTag[64], chatTag[32], color[64];
+
+ if (tagType == ProfileTagType_Admin)
+ {
+ FormatEx(clanTag, sizeof(clanTag), "[%s %T]", gC_ModeNamesShort[mode], "Tag - Admin", client);
+ FormatEx(chatTag, sizeof(chatTag), "%T", "Tag - Admin", client);
+ color = TAG_COLOR_ADMIN;
+ }
+ if (tagType == ProfileTagType_VIP)
+ {
+ FormatEx(clanTag, sizeof(clanTag), "[%s %T]", gC_ModeNamesShort[mode], "Tag - VIP", client);
+ FormatEx(chatTag, sizeof(chatTag), "%T", "Tag - VIP", client);
+ color = TAG_COLOR_VIP;
+ }
+
+ if (GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankClanTag]) != ProfileOptionBool_Enabled)
+ {
+ FormatEx(clanTag, sizeof(clanTag), "[%s]", gC_ModeNamesShort[mode]);
+ }
+ CS_SetClientClanTag(client, clanTag);
+
+ if (gB_Chat)
+ {
+ if (GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankChat]) == ProfileOptionBool_Enabled)
+ {
+ GOKZ_CH_SetChatTag(client, chatTag, color);
+ }
+ else
+ {
+ GOKZ_CH_SetChatTag(client, "", "{default}");
+ }
+ }
+ return;
+ }
+
+ int points = GOKZ_GL_GetRankPoints(client, mode);
+ int rank;
+ for (rank = 1; rank < RANK_COUNT; rank++)
+ {
+ if (points < gI_rankThreshold[mode][rank])
+ {
+ break;
+ }
+ }
+ rank--;
+
+ if (GOKZ_GetCoreOption(client, Option_Mode) == mode)
+ {
+ if (points == -1)
+ {
+ UpdateTags(client, -1, mode);
+ }
+ else
+ {
+ UpdateTags(client, rank, mode);
+ }
+ }
+
+ if (gI_Rank[client][mode] != rank)
+ {
+ gI_Rank[client][mode] = rank;
+ Call_OnRankUpdated(client, mode, rank);
+ }
+}
+
+void UpdateTags(int client, int rank, int mode)
+{
+ char str[64];
+ if (rank != -1 &&
+ GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankClanTag]) == ProfileOptionBool_Enabled)
+ {
+ FormatEx(str, sizeof(str), "[%s %s]", gC_ModeNamesShort[mode], gC_rankName[rank]);
+ CS_SetClientClanTag(client, str);
+ }
+ else
+ {
+ FormatEx(str, sizeof(str), "[%s]", gC_ModeNamesShort[mode]);
+ CS_SetClientClanTag(client, str);
+ }
+
+ if (gB_Chat)
+ {
+ if (rank != -1 &&
+ GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_ShowRankChat]) == ProfileOptionBool_Enabled)
+ {
+ GOKZ_CH_SetChatTag(client, gC_rankName[rank], gC_rankColor[rank]);
+ }
+ else
+ {
+ GOKZ_CH_SetChatTag(client, "", "{default}");
+ }
+ }
+}
+
+bool CanUseTagType(int client, int tagType)
+{
+ switch (tagType)
+ {
+ case ProfileTagType_Rank: return true;
+ case ProfileTagType_VIP: return CheckCommandAccess(client, "gokz_flag_vip", ADMFLAG_CUSTOM1);
+ case ProfileTagType_Admin: return CheckCommandAccess(client, "gokz_flag_admin", ADMFLAG_GENERIC);
+ default: return false;
+ }
+}
+
+int GetAvailableTagTypeOrDefault(int client)
+{
+ int tagType = GOKZ_GetOption(client, gC_ProfileOptionNames[ProfileOption_TagType]);
+ if (!CanUseTagType(client, tagType))
+ {
+ return ProfileTagType_Rank;
+ }
+
+ return tagType;
+}
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_profile", CommandProfile, "[KZ] Show the profile of a player. Usage: !profile <player>");
+ RegConsoleCmd("sm_p", CommandProfile, "[KZ] Show the profile of a player. Usage: !p <player>");
+ RegConsoleCmd("sm_profileoptions", CommandProfileOptions, "[KZ] Show the profile options.");
+ RegConsoleCmd("sm_pfo", CommandProfileOptions, "[KZ] Show the profile options.");
+ RegConsoleCmd("sm_ranks", CommandRanks, "[KZ] Show all the available ranks.");
+}
+
+public Action CommandProfile(int client, int args)
+{
+ if (args == 0)
+ {
+ ShowProfile(client, client);
+ }
+ else
+ {
+ char playerName[64];
+ GetCmdArgString(playerName, sizeof(playerName));
+ int player = FindTarget(client, playerName, true, false);
+ if (player != -1)
+ {
+ ShowProfile(client, player);
+ }
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandProfileOptions(int client, int args)
+{
+ DisplayProfileOptionsMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandRanks(int client, int args)
+{
+ char rankBuffer[256];
+ char buffer[256];
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+
+ Format(buffer, sizeof(buffer), "%s: ", gC_ModeNamesShort[mode]);
+
+ for (int i = 0; i < RANK_COUNT; i++) {
+ Format(rankBuffer, sizeof(rankBuffer), "%s%s (%d) ", gC_rankColor[i], gC_rankName[i], gI_rankThreshold[mode][i]);
+ StrCat(buffer, sizeof(buffer), rankBuffer);
+
+ if (i > 0 && i % 3 == 0) {
+ GOKZ_PrintToChat(client, true, buffer);
+ Format(buffer, sizeof(buffer), "%s: ", gC_ModeNamesShort[mode]);
+ }
+ }
+
+ GOKZ_PrintToChat(client, true, buffer);
+
+ return Plugin_Handled;
+}
+
+
+// =====[ FORWARDS ]=====
+
+static GlobalForward H_OnRankUpdated;
+
+
+void CreateGlobalForwards()
+{
+ H_OnRankUpdated = new GlobalForward("GOKZ_PF_OnRankUpdated", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
+}
+
+void Call_OnRankUpdated(int client, int mode, int rank)
+{
+ Call_StartForward(H_OnRankUpdated);
+ Call_PushCell(client);
+ Call_PushCell(mode);
+ Call_PushCell(rank);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_PF_GetRank", Native_GetRank);
+}
+
+public int Native_GetRank(Handle plugin, int numParams)
+{
+ return gI_Rank[GetNativeCell(1)][GetNativeCell(2)];
+}
diff --git a/sourcemod/scripting/gokz-profile/options.sp b/sourcemod/scripting/gokz-profile/options.sp
new file mode 100644
index 0000000..de8da51
--- /dev/null
+++ b/sourcemod/scripting/gokz-profile/options.sp
@@ -0,0 +1,128 @@
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void RegisterOptions()
+{
+ for (ProfileOption option; option < PROFILEOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_ProfileOptionNames[option], gC_ProfileOptionDescriptions[option],
+ OptionType_Int, gI_ProfileOptionDefaults[option], 0, gI_ProfileOptionCounts[option] - 1);
+ }
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatProfile;
+TopMenuObject gTMO_ItemsProfile[PROFILEOPTION_COUNT];
+
+void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu && gTMO_CatProfile != INVALID_TOPMENUOBJECT)
+ {
+ return;
+ }
+
+ gTMO_CatProfile = topMenu.AddCategory(PROFILE_OPTION_CATEGORY, TopMenuHandler_Categories);
+}
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ // Make sure category exists
+ if (gTMO_CatProfile == INVALID_TOPMENUOBJECT)
+ {
+ GOKZ_OnOptionsMenuCreated(topMenu);
+ }
+
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+
+ // Add gokz-profile option items
+ for (int option = 0; option < view_as<int>(PROFILEOPTION_COUNT); option++)
+ {
+ gTMO_ItemsProfile[option] = gTM_Options.AddItem(gC_ProfileOptionNames[option], TopMenuHandler_Profile, gTMO_CatProfile);
+ }
+}
+
+void DisplayProfileOptionsMenu(int client)
+{
+ if (gTMO_CatProfile != INVALID_TOPMENUOBJECT)
+ {
+ gTM_Options.DisplayCategory(gTMO_CatProfile, client);
+ }
+}
+
+public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == gTMO_CatProfile)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - Profile", param);
+ }
+ }
+}
+
+public void TopMenuHandler_Profile(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ ProfileOption option = PROFILEOPTION_INVALID;
+ for (int i = 0; i < view_as<int>(PROFILEOPTION_COUNT); i++)
+ {
+ if (topobj_id == gTMO_ItemsProfile[i])
+ {
+ option = view_as<ProfileOption>(i);
+ break;
+ }
+ }
+
+ if (option == PROFILEOPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ if (option == ProfileOption_TagType)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_ProfileOptionPhrases[option], param,
+ gC_ProfileTagTypePhrases[GOKZ_GetOption(param, gC_ProfileOptionNames[option])], param);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_ProfileOptionPhrases[option], param,
+ gC_ProfileBoolPhrases[GOKZ_GetOption(param, gC_ProfileOptionNames[option])], param);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_CycleOption(param, gC_ProfileOptionNames[option]);
+
+ if (option == ProfileOption_TagType)
+ {
+ for (int i = 0; i < PROFILETAGTYPE_COUNT; i++)
+ {
+ int tagType = GOKZ_GetOption(param, gC_ProfileOptionNames[option]);
+ if (!CanUseTagType(param, tagType))
+ {
+ GOKZ_CycleOption(param, gC_ProfileOptionNames[option]);
+ }
+ }
+ }
+
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
diff --git a/sourcemod/scripting/gokz-profile/profile.sp b/sourcemod/scripting/gokz-profile/profile.sp
new file mode 100644
index 0000000..fabdb0e
--- /dev/null
+++ b/sourcemod/scripting/gokz-profile/profile.sp
@@ -0,0 +1,222 @@
+
+#define ITEM_INFO_NAME "name"
+#define ITEM_INFO_MODE "mode"
+#define ITEM_INFO_RANK "rank"
+#define ITEM_INFO_POINTS "points"
+
+int profileTargetPlayer[MAXPLAYERS];
+int profileMode[MAXPLAYERS];
+bool profileWaitingForUpdate[MAXPLAYERS];
+
+
+
+// =====[ PUBLIC ]=====
+
+void ShowProfile(int client, int player = 0)
+{
+ if (player != 0)
+ {
+ profileTargetPlayer[client] = player;
+ profileMode[client] = GOKZ_GetCoreOption(player, Option_Mode);
+ }
+
+ if (GOKZ_GL_GetRankPoints(profileTargetPlayer[client], profileMode[client]) < 0)
+ {
+ if (!profileWaitingForUpdate[client])
+ {
+ GOKZ_GL_UpdatePoints(profileTargetPlayer[client], profileMode[client]);
+ profileWaitingForUpdate[client] = true;
+ }
+ return;
+ }
+
+ profileWaitingForUpdate[client] = false;
+ Menu menu = new Menu(MenuHandler_Profile);
+ menu.SetTitle("%T - %N", "Profile Menu - Title", client, profileTargetPlayer[client]);
+ ProfileMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void Profile_OnClientConnected(int client)
+{
+ profileTargetPlayer[client] = 0;
+ profileWaitingForUpdate[client] = false;
+}
+
+void Profile_OnClientDisconnect(int client)
+{
+ profileTargetPlayer[client] = 0;
+ profileWaitingForUpdate[client] = false;
+}
+
+void Profile_OnPointsUpdated(int player, int mode)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (profileWaitingForUpdate[client]
+ && profileTargetPlayer[client] == player
+ && profileMode[client] == mode)
+ {
+ ShowProfile(client);
+ }
+ }
+}
+
+public int MenuHandler_Profile(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_MODE, false))
+ {
+ if (++profileMode[param1] == MODE_COUNT)
+ {
+ profileMode[param1] = 0;
+ }
+ }
+ else if (StrEqual(info, ITEM_INFO_RANK, false))
+ {
+ ShowRankInfo(param1);
+ return 0;
+ }
+ else if (StrEqual(info, ITEM_INFO_POINTS, false))
+ {
+ ShowPointsInfo(param1);
+ return 0;
+ }
+
+ ShowProfile(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void ProfileMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+ int player = profileTargetPlayer[client];
+ int mode = profileMode[client];
+
+ FormatEx(display, sizeof(display), "%T: %s",
+ "Profile Menu - Mode", client, gC_ModeNames[mode]);
+ menu.AddItem(ITEM_INFO_MODE, display);
+
+ FormatEx(display, sizeof(display), "%T: %s",
+ "Profile Menu - Rank", client, gC_rankName[gI_Rank[player][mode]]);
+ menu.AddItem(ITEM_INFO_RANK, display);
+
+ FormatEx(display, sizeof(display), "%T: %d",
+ "Profile Menu - Points", client, GOKZ_GL_GetRankPoints(player, mode));
+ menu.AddItem(ITEM_INFO_POINTS, display);
+}
+
+static void ShowRankInfo(int client)
+{
+ Menu menu = new Menu(MenuHandler_RankInfo);
+ menu.SetTitle("%T - %N", "Rank Info Menu - Title", client, profileTargetPlayer[client]);
+ RankInfoMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void RankInfoMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+ int player = profileTargetPlayer[client];
+ int mode = profileMode[client];
+
+ FormatEx(display, sizeof(display), "%T: %s",
+ "Rank Info Menu - Current Rank", client, gC_rankName[gI_Rank[player][mode]]);
+ menu.AddItem("", display);
+
+ int next_rank = gI_Rank[player][mode] + 1;
+ if (next_rank == RANK_COUNT)
+ {
+ FormatEx(display, sizeof(display), "%T: -",
+ "Rank Info Menu - Next Rank", client);
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T: 0",
+ "Rank Info Menu - Points needed", client);
+ menu.AddItem("", display);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T: %s",
+ "Rank Info Menu - Next Rank", client, gC_rankName[next_rank]);
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T: %d",
+ "Rank Info Menu - Points needed", client, gI_rankThreshold[mode][next_rank] - GOKZ_GL_GetRankPoints(player, mode));
+ menu.AddItem("", display);
+ }
+}
+
+static int MenuHandler_RankInfo(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Cancel)
+ {
+ ShowProfile(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+static void ShowPointsInfo(int client)
+{
+ Menu menu = new Menu(MenuHandler_PointsInfo);
+ menu.SetTitle("%T - %N", "Points Info Menu - Title", client, profileTargetPlayer[client]);
+ PointsInfoMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+static void PointsInfoMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+ int player = profileTargetPlayer[client];
+ int mode = profileMode[client];
+
+ FormatEx(display, sizeof(display), "%T: %d",
+ "Points Info Menu - Overall Points", client, GOKZ_GL_GetPoints(player, mode, TimeType_Nub));
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T: %d",
+ "Points Info Menu - Pro Points", client, GOKZ_GL_GetPoints(player, mode, TimeType_Pro));
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T: %d",
+ "Points Info Menu - Overall Completion", client, GOKZ_GL_GetFinishes(player, mode, TimeType_Nub));
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T: %d",
+ "Points Info Menu - Pro Completion", client, GOKZ_GL_GetFinishes(player, mode, TimeType_Pro));
+ menu.AddItem("", display);
+}
+
+static int MenuHandler_PointsInfo(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Cancel)
+ {
+ ShowProfile(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
diff --git a/sourcemod/scripting/gokz-quiet.sp b/sourcemod/scripting/gokz-quiet.sp
new file mode 100644
index 0000000..06cc246
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet.sp
@@ -0,0 +1,151 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdkhooks>
+#include <dhooks>
+
+#include <gokz/core>
+#include <gokz/quiet>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Quiet",
+ author = "DanZay",
+ description = "Provides options for a quieter KZ experience",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-quiet.txt"
+
+
+#include "gokz-quiet/ambient.sp"
+#include "gokz-quiet/soundscape.sp"
+#include "gokz-quiet/hideplayers.sp"
+#include "gokz-quiet/falldamage.sp"
+#include "gokz-quiet/gokz-sounds.sp"
+#include "gokz-quiet/options.sp"
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-quiet");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ OnPluginStart_HidePlayers();
+ OnPluginStart_FallDamage();
+ OnPluginStart_Ambient();
+
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-quiet.phrases");
+
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ GOKZ_OnJoinTeam(client, GetClientTeam(client));
+ }
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void GOKZ_OnJoinTeam(int client, int team)
+{
+ OnJoinTeam_HidePlayers(client, team);
+}
+
+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_Soundscape(client);
+}
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ any qtOption;
+ if (GOKZ_QT_IsQTOption(option, qtOption))
+ {
+ OnOptionChanged_Options(client, qtOption, newValue);
+ }
+}
+
+public void GOKZ_OnOptionsMenuCreated(TopMenu topMenu)
+{
+ OnOptionsMenuCreated_OptionsMenu(topMenu);
+}
+
+// =====[ STOP SOUNDS ]=====
+
+void StopSounds(int client)
+{
+ ClientCommand(client, "snd_playsounds Music.StopAllExceptMusic");
+ GOKZ_PrintToChat(client, true, "%t", "Stopped Sounds");
+}
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_hide", CommandToggleShowPlayers, "[KZ] Toggle the visibility of other players.");
+ RegConsoleCmd("sm_stopsound", CommandStopSound, "[KZ] Stop all sounds e.g. map soundscapes (music).");
+}
+
+public Action CommandStopSound(int client, int args)
+{
+ StopSounds(client);
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-quiet/ambient.sp b/sourcemod/scripting/gokz-quiet/ambient.sp
new file mode 100644
index 0000000..a67e20e
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet/ambient.sp
@@ -0,0 +1,100 @@
+/*
+ Hide sound effect from ambient_generics.
+ Credit to Haze - https://github.com/Haze1337/Sound-Manager
+*/
+
+Handle getPlayerSlot;
+
+void OnPluginStart_Ambient()
+{
+ HookSendSound();
+}
+static void HookSendSound()
+{
+ GameData gd = LoadGameConfigFile("gokz-quiet.games");
+
+ DynamicDetour sendSoundDetour = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Void, ThisPointer_Address);
+ DHookSetFromConf(sendSoundDetour, gd, SDKConf_Signature, "CGameClient::SendSound");
+ DHookAddParam(sendSoundDetour, HookParamType_ObjectPtr);
+ DHookAddParam(sendSoundDetour, HookParamType_Bool);
+ if (!DHookEnableDetour(sendSoundDetour, false, DHooks_OnSendSound))
+ {
+ SetFailState("Couldn't enable CGameClient::SendSound detour.");
+ }
+
+ StartPrepSDKCall(SDKCall_Raw);
+ PrepSDKCall_SetFromConf(gd, SDKConf_Virtual, "CBaseClient::GetPlayerSlot");
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+ getPlayerSlot = EndPrepSDKCall();
+ if (getPlayerSlot == null)
+ {
+ SetFailState("Could not initialize call to CBaseClient::GetPlayerSlot.");
+ }
+}
+
+
+/*struct SoundInfo_t
+{
+ Vector vOrigin; Offset: 0 | Size: 12
+ Vector vDirection Offset: 12 | Size: 12
+ Vector vListenerOrigin; Offset: 24 | Size: 12
+ const char *pszName; Offset: 36 | Size: 4
+ float fVolume; Offset: 40 | Size: 4
+ float fDelay; Offset: 44 | Size: 4
+ float fTickTime; Offset: 48 | Size: 4
+ int nSequenceNumber; Offset: 52 | Size: 4
+ int nEntityIndex; Offset: 56 | Size: 4
+ int nChannel; Offset: 60 | Size: 4
+ int nPitch; Offset: 64 | Size: 4
+ int nFlags; Offset: 68 | Size: 4
+ unsigned int nSoundNum; Offset: 72 | Size: 4
+ int nSpeakerEntity; Offset: 76 | Size: 4
+ int nRandomSeed; Offset: 80 | Size: 4
+ soundlevel_t Soundlevel; Offset: 84 | Size: 4
+ bool bIsSentence; Offset: 88 | Size: 1
+ bool bIsAmbient; Offset: 89 | Size: 1
+ bool bLooping; Offset: 90 | Size: 1
+};*/
+
+//void CGameClient::SendSound( SoundInfo_t &sound, bool isReliable )
+public MRESReturn DHooks_OnSendSound(Address pThis, Handle hParams)
+{
+ // Check volume
+ float volume = DHookGetParamObjectPtrVar(hParams, 1, 40, ObjectValueType_Float);
+ if(volume == 0.0)
+ {
+ return MRES_Ignored;
+ }
+
+ Address pIClient = pThis + view_as<Address>(0x4);
+ int client = view_as<int>(SDKCall(getPlayerSlot, pIClient)) + 1;
+
+ if(!IsValidClient(client))
+ {
+ return MRES_Ignored;
+ }
+
+ bool isAmbient = DHookGetParamObjectPtrVar(hParams, 1, 89, ObjectValueType_Bool);
+ if (!isAmbient)
+ {
+ return MRES_Ignored;
+ }
+
+ float newVolume;
+ if (GOKZ_QT_GetOption(client, QTOption_AmbientSounds) == -1 || GOKZ_QT_GetOption(client, QTOption_AmbientSounds) == 10)
+ {
+ newVolume = volume;
+ }
+ else
+ {
+ float volumeFactor = float(GOKZ_QT_GetOption(client, QTOption_AmbientSounds)) * 0.1;
+ newVolume = volume * volumeFactor;
+ }
+
+ if (newVolume <= 0.0)
+ {
+ return MRES_Supercede;
+ }
+ DHookSetParamObjectPtrVar(hParams, 1, 40, ObjectValueType_Float, newVolume);
+ return MRES_ChangedHandled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-quiet/falldamage.sp b/sourcemod/scripting/gokz-quiet/falldamage.sp
new file mode 100644
index 0000000..6bd0533
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet/falldamage.sp
@@ -0,0 +1,40 @@
+/*
+ Toggle player's fall damage sounds.
+*/
+
+void OnPluginStart_FallDamage()
+{
+ AddNormalSoundHook(Hook_NormalSound);
+}
+
+static Action Hook_NormalSound(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 (!StrEqual(soundEntry, "Player.FallDamage"))
+ {
+ return Plugin_Continue;
+ }
+
+ for (int i = 0; i < numClients; i++)
+ {
+ int client = clients[i];
+ if (!IsValidClient(client))
+ {
+ continue;
+ }
+ int clientArray[1];
+ clientArray[0] = client;
+ float newVolume;
+ if (GOKZ_QT_GetOption(client, QTOption_FallDamageSound) == -1 || GOKZ_QT_GetOption(client, QTOption_FallDamageSound) == 10)
+ {
+ newVolume = volume;
+ }
+ else
+ {
+ float volumeFactor = float(GOKZ_QT_GetOption(client, QTOption_FallDamageSound)) * 0.1;
+ newVolume = volume * volumeFactor;
+ }
+
+ EmitSoundEntry(clientArray, 1, soundEntry, sample, entity, channel, level, seed, flags, newVolume, pitch);
+ }
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-quiet/gokz-sounds.sp b/sourcemod/scripting/gokz-quiet/gokz-sounds.sp
new file mode 100644
index 0000000..02cf681
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet/gokz-sounds.sp
@@ -0,0 +1,71 @@
+/*
+ Volume options for various GOKZ sounds.
+*/
+
+public Action GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description)
+{
+ int volumeFactor = 10;
+ if (StrEqual(description, "Checkpoint") || StrEqual(description, "Set Start Position"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_CheckpointVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ else if (StrEqual(description, "Checkpoint"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_TeleportVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ else if (StrEqual(description, "Timer Start") || StrEqual(description, "Timer End") || StrEqual(description, "Timer False End") || StrEqual(description, "Missed PB"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_TimerVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ else if (StrEqual(description, "Error"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_ErrorVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ else if (StrEqual(description, "Server Record"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_ServerRecordVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ else if (StrEqual(description, "World Record"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_WorldRecordVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ else if (StrEqual(description, "Jumpstats"))
+ {
+ volumeFactor = GOKZ_QT_GetOption(client, QTOption_JumpstatsVolume);
+ if (volumeFactor == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+
+ if (volumeFactor == 10)
+ {
+ return Plugin_Continue;
+ }
+ volume *= float(volumeFactor) * 0.1;
+ return Plugin_Changed;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-quiet/hideplayers.sp b/sourcemod/scripting/gokz-quiet/hideplayers.sp
new file mode 100644
index 0000000..65736f0
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet/hideplayers.sp
@@ -0,0 +1,309 @@
+/*
+ Hide sounds and effects from other players.
+*/
+
+void OnPluginStart_HidePlayers()
+{
+ AddNormalSoundHook(Hook_NormalSound);
+ AddTempEntHook("Shotgun Shot", Hook_ShotgunShot);
+ AddTempEntHook("EffectDispatch", Hook_EffectDispatch);
+ HookUserMessage(GetUserMessageId("WeaponSound"), Hook_WeaponSound, true);
+
+ // Lateload support
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client))
+ {
+ OnJoinTeam_HidePlayers(client, GetClientTeam(client));
+ }
+ }
+}
+
+void OnJoinTeam_HidePlayers(int client, int team)
+{
+ // Make sure client is only ever hooked once
+ SDKUnhook(client, SDKHook_SetTransmit, OnSetTransmitClient);
+
+ if (team == CS_TEAM_T || team == CS_TEAM_CT)
+ {
+ SDKHook(client, SDKHook_SetTransmit, OnSetTransmitClient);
+ }
+}
+
+Action CommandToggleShowPlayers(int client, int args)
+{
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Disabled)
+ {
+ GOKZ_SetOption(client, gC_QTOptionNames[QTOption_ShowPlayers], ShowPlayers_Enabled);
+ }
+ else
+ {
+ GOKZ_SetOption(client, gC_QTOptionNames[QTOption_ShowPlayers], ShowPlayers_Disabled);
+ }
+ return Plugin_Handled;
+}
+
+// =====[ PRIVATE ]=====
+
+// Hide most of the other players' actions. This function is expensive.
+static Action OnSetTransmitClient(int entity, int client)
+{
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Disabled
+ && entity != client
+ && entity != GetObserverTarget(client))
+ {
+ return Plugin_Handled;
+ }
+ return Plugin_Continue;
+}
+
+// Hide reload sounds. Required if other players were visible at one point during the gameplay.
+static Action Hook_WeaponSound(UserMsg msg_id, Protobuf msg, const int[] players, int playersNum, bool reliable, bool init)
+{
+ int newClients[MAXPLAYERS], newTotal = 0;
+ int entidx = msg.ReadInt("entidx");
+ for (int i = 0; i < playersNum; i++)
+ {
+ int client = players[i];
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled
+ || entidx == client
+ || entidx == GetObserverTarget(client))
+ {
+ newClients[newTotal] = client;
+ newTotal++;
+ }
+ }
+
+ // Nothing's changed, let the engine handle it.
+ if (newTotal == playersNum)
+ {
+ return Plugin_Continue;
+ }
+ // No one to send to so it doesn't matter if we block or not. We block just to end the function early.
+ if (newTotal == 0)
+ {
+ return Plugin_Handled;
+ }
+ // Only way to modify the recipient list is to RequestFrame and create our own user message.
+ char path[PLATFORM_MAX_PATH];
+ msg.ReadString("sound", path, sizeof(path));
+ int flags = USERMSG_BLOCKHOOKS;
+ if (reliable)
+ {
+ flags |= USERMSG_RELIABLE;
+ }
+ if (init)
+ {
+ flags |= USERMSG_INITMSG;
+ }
+
+ DataPack dp = new DataPack();
+ dp.WriteCell(msg_id);
+ dp.WriteCell(newTotal);
+ dp.WriteCellArray(newClients, newTotal);
+ dp.WriteCell(flags);
+ dp.WriteCell(entidx);
+ dp.WriteFloat(msg.ReadFloat("origin_x"));
+ dp.WriteFloat(msg.ReadFloat("origin_y"));
+ dp.WriteFloat(msg.ReadFloat("origin_z"));
+ dp.WriteString(path);
+ dp.WriteFloat(msg.ReadFloat("timestamp"));
+
+ RequestFrame(RequestFrame_WeaponSound, dp);
+ return Plugin_Handled;
+}
+
+static void RequestFrame_WeaponSound(DataPack dp)
+{
+ dp.Reset();
+
+ UserMsg msg_id = dp.ReadCell();
+ int newTotal = dp.ReadCell();
+ int newClients[MAXPLAYERS];
+ dp.ReadCellArray(newClients, newTotal);
+ int flags = dp.ReadCell();
+
+ Protobuf newMsg = view_as<Protobuf>(StartMessageEx(msg_id, newClients, newTotal, flags));
+
+ newMsg.SetInt("entidx", dp.ReadCell());
+ newMsg.SetFloat("origin_x", dp.ReadFloat());
+ newMsg.SetFloat("origin_y", dp.ReadFloat());
+ newMsg.SetFloat("origin_z", dp.ReadFloat());
+ char path[PLATFORM_MAX_PATH];
+ dp.ReadString(path, sizeof(path));
+ newMsg.SetString("sound", path);
+ newMsg.SetFloat("timestamp", dp.ReadFloat());
+
+ EndMessage();
+
+ delete dp;
+}
+
+// Hide various sounds that don't get blocked by SetTransmit hook.
+static Action Hook_NormalSound(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 (StrContains(sample, "Player.EquipArmor") != -1 || StrContains(sample, "BaseCombatCharacter.AmmoPickup") != -1)
+ {
+ // When the sound is emitted, the owner of these entities are not set yet.
+ // Hence we cannot do the entity parent stuff below.
+ // In that case, we just straight up block armor and ammo pickup sounds.
+ return Plugin_Stop;
+ }
+ int ent = entity;
+ while (ent > MAXPLAYERS)
+ {
+ // Block some gun and knife sounds by trying to find its parent entity.
+ ent = GetEntPropEnt(ent, Prop_Send, "moveparent");
+ if (ent < MAXPLAYERS)
+ {
+ break;
+ }
+ else if (ent == -1)
+ {
+ return Plugin_Continue;
+ }
+ }
+ int numNewClients = 0;
+ for (int i = 0; i < numClients; i++)
+ {
+ int client = clients[i];
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled
+ || ent == client
+ || ent == GetObserverTarget(client))
+ {
+ clients[numNewClients] = client;
+ numNewClients++;
+ }
+ }
+
+ if (numNewClients != numClients)
+ {
+ numClients = numNewClients;
+ return Plugin_Changed;
+ }
+
+ return Plugin_Continue;
+}
+
+// Hide firing sounds.
+static Action Hook_ShotgunShot(const char[] te_name, const int[] players, int numClients, float delay)
+{
+ int newClients[MAXPLAYERS], newTotal = 0;
+ for (int i = 0; i < numClients; i++)
+ {
+ int client = players[i];
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled
+ || TE_ReadNum("m_iPlayer") + 1 == GetObserverTarget(client))
+ {
+ newClients[newTotal] = client;
+ newTotal++;
+ }
+ }
+
+ // Noone wants the sound
+ if (newTotal == 0)
+ {
+ return Plugin_Stop;
+ }
+
+ // Nothing's changed, let the engine handle it.
+ if (newTotal == numClients)
+ {
+ return Plugin_Continue;
+ }
+
+ float origin[3];
+ TE_ReadVector("m_vecOrigin", origin);
+
+ float angles[2];
+ angles[0] = TE_ReadFloat("m_vecAngles[0]");
+ angles[1] = TE_ReadFloat("m_vecAngles[1]");
+
+ int weapon = TE_ReadNum("m_weapon");
+ int mode = TE_ReadNum("m_iMode");
+ int seed = TE_ReadNum("m_iSeed");
+ int player = TE_ReadNum("m_iPlayer");
+ float inaccuracy = TE_ReadFloat("m_fInaccuracy");
+ float recoilIndex = TE_ReadFloat("m_flRecoilIndex");
+ float spread = TE_ReadFloat("m_fSpread");
+ int itemIdx = TE_ReadNum("m_nItemDefIndex");
+ int soundType = TE_ReadNum("m_iSoundType");
+
+ TE_Start("Shotgun Shot");
+ TE_WriteVector("m_vecOrigin", origin);
+ TE_WriteFloat("m_vecAngles[0]", angles[0]);
+ TE_WriteFloat("m_vecAngles[1]", angles[1]);
+ TE_WriteNum("m_weapon", weapon);
+ TE_WriteNum("m_iMode", mode);
+ TE_WriteNum("m_iSeed", seed);
+ TE_WriteNum("m_iPlayer", player);
+ TE_WriteFloat("m_fInaccuracy", inaccuracy);
+ TE_WriteFloat("m_flRecoilIndex", recoilIndex);
+ TE_WriteFloat("m_fSpread", spread);
+ TE_WriteNum("m_nItemDefIndex", itemIdx);
+ TE_WriteNum("m_iSoundType", soundType);
+
+ // Send the TE and stop the engine from processing its own.
+ TE_Send(newClients, newTotal, delay);
+ return Plugin_Stop;
+}
+
+// Hide knife and blood effect caused by other players.
+static Action Hook_EffectDispatch(const char[] te_name, const int[] players, int numClients, float delay)
+{
+ // Block bullet impact effects.
+ int effIndex = TE_ReadNum("m_iEffectName");
+ if (effIndex != EFFECT_IMPACT && effIndex != EFFECT_KNIFESLASH)
+ {
+ return Plugin_Continue;
+ }
+ int newClients[MAXPLAYERS], newTotal = 0;
+ for (int i = 0; i < numClients; i++)
+ {
+ int client = players[i];
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_ShowPlayers]) == ShowPlayers_Enabled)
+ {
+ newClients[newTotal] = client;
+ newTotal++;
+ }
+ }
+ // Noone wants the sound
+ if (newTotal == 0)
+ {
+ return Plugin_Stop;
+ }
+
+ // Nothing's changed, let the engine handle it.
+ if (newTotal == numClients)
+ {
+ return Plugin_Continue;
+ }
+ float origin[3], start[3];
+ origin[0] = TE_ReadFloat("m_vOrigin.x");
+ origin[1] = TE_ReadFloat("m_vOrigin.y");
+ origin[2] = TE_ReadFloat("m_vOrigin.z");
+ start[0] = TE_ReadFloat("m_vStart.x");
+ start[1] = TE_ReadFloat("m_vStart.y");
+ start[2] = TE_ReadFloat("m_vStart.z");
+ int flags = TE_ReadNum("m_fFlags");
+ float scale = TE_ReadFloat("m_flScale");
+ int surfaceProp = TE_ReadNum("m_nSurfaceProp");
+ int damageType = TE_ReadNum("m_nDamageType");
+ int entindex = TE_ReadNum("entindex");
+ int positionsAreRelativeToEntity = TE_ReadNum("m_bPositionsAreRelativeToEntity");
+
+ TE_Start("EffectDispatch");
+ TE_WriteNum("m_iEffectName", effIndex);
+ TE_WriteFloatArray("m_vOrigin.x", origin, 3);
+ TE_WriteFloatArray("m_vStart.x", start, 3);
+ TE_WriteFloat("m_flScale", scale);
+ TE_WriteNum("m_nSurfaceProp", surfaceProp);
+ TE_WriteNum("m_nDamageType", damageType);
+ TE_WriteNum("entindex", entindex);
+ TE_WriteNum("m_bPositionsAreRelativeToEntity", positionsAreRelativeToEntity);
+ TE_WriteNum("m_fFlags", flags);
+
+ // Send the TE and stop the engine from processing its own.
+ TE_Send(newClients, newTotal, delay);
+ return Plugin_Stop;
+}
diff --git a/sourcemod/scripting/gokz-quiet/options.sp b/sourcemod/scripting/gokz-quiet/options.sp
new file mode 100644
index 0000000..a9da3d7
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet/options.sp
@@ -0,0 +1,206 @@
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOptions();
+}
+
+void RegisterOptions()
+{
+ for (QTOption option; option < QTOPTION_COUNT; option++)
+ {
+ GOKZ_RegisterOption(gC_QTOptionNames[option], gC_QTOptionDescriptions[option],
+ OptionType_Int, gI_QTOptionDefaultValues[option], 0, gI_QTOptionCounts[option] - 1);
+ }
+}
+
+void OnOptionChanged_Options(int client, QTOption option, any newValue)
+{
+ if (option == QTOption_Soundscapes && newValue == Soundscapes_Enabled)
+ {
+ EnableSoundscape(client);
+ }
+ PrintOptionChangeMessage(client, option, newValue);
+}
+
+void PrintOptionChangeMessage(int client, QTOption option, any newValue)
+{
+ switch (option)
+ {
+ case QTOption_ShowPlayers:
+ {
+ switch (newValue)
+ {
+ case ShowPlayers_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Players - Disable");
+ }
+ case ShowPlayers_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Show Players - Enable");
+ }
+ }
+ }
+ case QTOption_Soundscapes:
+ {
+ switch (newValue)
+ {
+ case Soundscapes_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Soundscapes - Disable");
+ }
+ case Soundscapes_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Soundscapes - Enable");
+ }
+ }
+ }
+ }
+}
+
+// =====[ OPTIONS MENU ]=====
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatQuiet;
+TopMenuObject gTMO_ItemsQuiet[QTOPTION_COUNT];
+
+void OnOptionsMenuCreated_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu && gTMO_CatQuiet != INVALID_TOPMENUOBJECT)
+ {
+ return;
+ }
+
+ gTMO_CatQuiet = topMenu.AddCategory(QUIET_OPTION_CATEGORY, TopMenuHandler_Categories);
+}
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ // Make sure category exists
+ if (gTMO_CatQuiet == INVALID_TOPMENUOBJECT)
+ {
+ GOKZ_OnOptionsMenuCreated(topMenu);
+ }
+
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+
+ // Add gokz-profile option items
+ for (int option = 0; option < view_as<int>(QTOPTION_COUNT); option++)
+ {
+ gTMO_ItemsQuiet[option] = gTM_Options.AddItem(gC_QTOptionNames[option], TopMenuHandler_QT, gTMO_CatQuiet);
+ }
+}
+
+public void TopMenuHandler_Categories(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (action == TopMenuAction_DisplayOption || action == TopMenuAction_DisplayTitle)
+ {
+ if (topobj_id == gTMO_CatQuiet)
+ {
+ Format(buffer, maxlength, "%T", "Options Menu - Quiet", param);
+ }
+ }
+}
+
+public void TopMenuHandler_QT(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ QTOption option = QTOPTION_INVALID;
+ for (int i = 0; i < view_as<int>(QTOPTION_COUNT); i++)
+ {
+ if (topobj_id == gTMO_ItemsQuiet[i])
+ {
+ option = view_as<QTOption>(i);
+ break;
+ }
+ }
+
+ if (option == QTOPTION_INVALID)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ switch (option)
+ {
+ case QTOption_ShowPlayers:
+ {
+ FormatToggleableOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_Soundscapes:
+ {
+ FormatToggleableOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_FallDamageSound:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_AmbientSounds:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_CheckpointVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_TeleportVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_TimerVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_ErrorVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_ServerRecordVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_WorldRecordVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ case QTOption_JumpstatsVolume:
+ {
+ FormatVolumeOptionDisplay(param, option, buffer, maxlength);
+ }
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_CycleOption(param, gC_QTOptionNames[option]);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
+void FormatToggleableOptionDisplay(int client, QTOption option, char[] buffer, int maxlength)
+{
+ if (GOKZ_GetOption(client, gC_QTOptionNames[option]) == 0)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_QTOptionPhrases[option], client,
+ "Options Menu - Disabled", client);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ gC_QTOptionPhrases[option], client,
+ "Options Menu - Enabled", client);
+ }
+}
+
+void FormatVolumeOptionDisplay(int client, QTOption option, char[] buffer, int maxlength)
+{
+ // Assume 10% volume steps.
+ FormatEx(buffer, maxlength, "%T - %i%",
+ gC_QTOptionPhrases[option], client,
+ GOKZ_QT_GetOption(client, option) * 10);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-quiet/soundscape.sp b/sourcemod/scripting/gokz-quiet/soundscape.sp
new file mode 100644
index 0000000..f320ad3
--- /dev/null
+++ b/sourcemod/scripting/gokz-quiet/soundscape.sp
@@ -0,0 +1,30 @@
+/*
+ Toggle soundscapes.
+*/
+
+static int currentSoundscapeIndex[MAXPLAYERS + 1] = {BLANK_SOUNDSCAPEINDEX, ...};
+
+void EnableSoundscape(int client)
+{
+ if (currentSoundscapeIndex[client] != BLANK_SOUNDSCAPEINDEX)
+ {
+ SetEntProp(client, Prop_Data, "soundscapeIndex", currentSoundscapeIndex[client]);
+ }
+}
+
+void OnPlayerRunCmdPost_Soundscape(int client)
+{
+ int soundscapeIndex = GetEntProp(client, Prop_Data, "soundscapeIndex");
+ if (GOKZ_GetOption(client, gC_QTOptionNames[QTOption_Soundscapes]) == Soundscapes_Disabled)
+ {
+ if (soundscapeIndex != BLANK_SOUNDSCAPEINDEX)
+ {
+ currentSoundscapeIndex[client] = soundscapeIndex;
+ }
+ SetEntProp(client, Prop_Data, "soundscapeIndex", BLANK_SOUNDSCAPEINDEX);
+ }
+ else
+ {
+ currentSoundscapeIndex[client] = soundscapeIndex;
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing.sp b/sourcemod/scripting/gokz-racing.sp
new file mode 100644
index 0000000..416d28a
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing.sp
@@ -0,0 +1,174 @@
+#include <sourcemod>
+
+#include <gokz/core>
+#include <gokz/racing>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Racing",
+ author = "DanZay",
+ description = "Lets players race against each other",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-racing.txt"
+
+#include "gokz-racing/announce.sp"
+#include "gokz-racing/api.sp"
+#include "gokz-racing/commands.sp"
+#include "gokz-racing/duel_menu.sp"
+#include "gokz-racing/race.sp"
+#include "gokz-racing/race_menu.sp"
+#include "gokz-racing/racer.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-racing");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-racing.phrases");
+
+ CreateGlobalForwards();
+ RegisterCommands();
+
+ OnPluginStart_Race();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ OnClientPutInServer_Racer(client);
+}
+
+public void OnClientDisconnect(int client)
+{
+ OnClientDisconnect_Racer(client);
+}
+
+public Action GOKZ_OnTimerStart(int client, int course)
+{
+ Action action = OnTimerStart_Racer(client, course);
+ if (action != Plugin_Continue)
+ {
+ return action;
+ }
+
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnTimerStart_Post(int client, int course)
+{
+ OnTimerStart_Post_Racer(client);
+}
+
+public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ FinishRacer(client, course);
+}
+
+public Action GOKZ_OnMakeCheckpoint(int client)
+{
+ Action action = OnMakeCheckpoint_Racer(client);
+ if (action != Plugin_Continue)
+ {
+ return action;
+ }
+
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnMakeCheckpoint_Post(int client)
+{
+ OnMakeCheckpoint_Post_Racer(client);
+}
+
+public Action GOKZ_OnUndoTeleport(int client)
+{
+ Action action = OnUndoTeleport_Racer(client);
+ if (action != Plugin_Continue)
+ {
+ return action;
+ }
+
+ return Plugin_Continue;
+}
+
+public void GOKZ_RC_OnFinish(int client, int raceID, int place)
+{
+ OnFinish_Announce(client, raceID, place);
+ OnFinish_Race(raceID);
+}
+
+public void GOKZ_RC_OnSurrender(int client, int raceID)
+{
+ OnSurrender_Announce(client, raceID);
+}
+
+public void GOKZ_RC_OnRequestReceived(int client, int raceID)
+{
+ OnRequestReceived_Announce(client, raceID);
+}
+
+public void GOKZ_RC_OnRequestAccepted(int client, int raceID)
+{
+ OnRequestAccepted_Announce(client, raceID);
+ OnRequestAccepted_Race(raceID);
+}
+
+public void GOKZ_RC_OnRequestDeclined(int client, int raceID, bool timeout)
+{
+ OnRequestDeclined_Announce(client, raceID, timeout);
+ OnRequestDeclined_Race(raceID);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void GOKZ_RC_OnRaceStarted(int raceID)
+{
+ OnRaceStarted_Announce(raceID);
+}
+
+public void GOKZ_RC_OnRaceAborted(int raceID)
+{
+ OnRaceAborted_Announce(raceID);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/announce.sp b/sourcemod/scripting/gokz-racing/announce.sp
new file mode 100644
index 0000000..7b6a920
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/announce.sp
@@ -0,0 +1,229 @@
+/*
+ Chat messages of race and racer events.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+/**
+ * Prints a message to chat for all clients in a race, formatting colours
+ * and optionally adding the chat prefix. If using the chat prefix, specify
+ * a colour at the beginning of the message e.g. "{default}Hello!".
+ *
+ * @param raceID ID of the race.
+ * @param specs Whether to also include racer spectators.
+ * @param addPrefix Whether to add the chat prefix.
+ * @param format Formatting rules.
+ * @param any Variable number of format parameters.
+ */
+void PrintToChatAllInRace(int raceID, bool specs, bool addPrefix, const char[] format, any...)
+{
+ char buffer[1024];
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && GetRaceID(client) == raceID)
+ {
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), format, 5);
+ GOKZ_PrintToChat(client, addPrefix, buffer);
+
+ if (specs)
+ {
+ for (int target = 1; target <= MaxClients; target++)
+ {
+ if (IsClientInGame(target) && GetObserverTarget(target) == client && GetRaceID(target) != raceID)
+ {
+ SetGlobalTransTarget(target);
+ VFormat(buffer, sizeof(buffer), format, 5);
+ GOKZ_PrintToChat(target, addPrefix, buffer);
+ }
+ }
+ }
+ }
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnFinish_Announce(int client, int raceID, int place)
+{
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ if (place == 1)
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Won", client);
+ }
+ else
+ {
+ ArrayList unfinishedRacers = GetUnfinishedRacers(raceID);
+ if (unfinishedRacers.Length >= 1)
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Placed", client, place);
+ }
+ else
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Lost", client, place);
+ }
+ delete unfinishedRacers;
+ }
+ }
+ case RaceType_Duel:
+ {
+ ArrayList unfinishedRacers = GetUnfinishedRacers(raceID);
+ if (unfinishedRacers.Length == 1)
+ {
+ int opponent = unfinishedRacers.Get(0);
+ GOKZ_PrintToChatAll(true, "%t", "Duel Won", client, opponent);
+ }
+ delete unfinishedRacers;
+ }
+ }
+}
+
+void OnSurrender_Announce(int client, int raceID)
+{
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Surrendered", client);
+ }
+ case RaceType_Duel:
+ {
+ ArrayList unfinishedRacers = GetUnfinishedRacers(raceID);
+ if (unfinishedRacers.Length == 1)
+ {
+ int opponent = unfinishedRacers.Get(0);
+ GOKZ_PrintToChatAll(true, "%t", "Duel Surrendered", client, opponent);
+ }
+ delete unfinishedRacers;
+ }
+ }
+}
+
+void OnRequestReceived_Announce(int client, int raceID)
+{
+ int host = GetRaceHost(raceID);
+
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Request Received", host);
+ }
+ case RaceType_Duel:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Duel Request Received", host);
+ }
+ }
+
+ int cpRule = GetRaceInfo(raceID, RaceInfo_CheckpointRule);
+ int cdRule = GetRaceInfo(raceID, RaceInfo_CooldownRule);
+ int mode = GetRaceInfo(raceID, RaceInfo_Mode);
+ int course = GetRaceInfo(raceID, RaceInfo_Course);
+
+ char courseStr[32];
+ if (course == 0)
+ {
+ FormatEx(courseStr, sizeof(courseStr), "%T", "Race Rules - Main Course", client);
+ }
+ else
+ {
+ FormatEx(courseStr, sizeof(courseStr), "%T %d", "Race Rules - Bonus Course", client, course);
+ }
+
+ if (cpRule == -1 && cdRule == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Unlimited", gC_ModeNames[mode], courseStr);
+ }
+ if (cpRule == -1 && cdRule > 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited Cooldown", gC_ModeNames[mode], courseStr, cdRule);
+ }
+ if (cpRule == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - No Checkpoints", gC_ModeNames[mode], courseStr);
+ }
+ if (cpRule > 0 && cdRule == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited Checkpoints", gC_ModeNames[mode], courseStr, cpRule);
+ }
+ if (cpRule > 0 && cdRule > 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Race Rules - Limited", gC_ModeNames[mode], courseStr, cpRule, cdRule);
+ }
+
+ GOKZ_PrintToChat(client, false, "%t", "You Have Seconds To Accept", RoundFloat(RC_REQUEST_TIMEOUT_TIME));
+}
+
+void OnRequestAccepted_Announce(int client, int raceID)
+{
+ int host = GetRaceHost(raceID);
+
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Request Accepted", client, host);
+ }
+ case RaceType_Duel:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Duel Request Accepted", client, host);
+ }
+ }
+}
+
+void OnRequestDeclined_Announce(int client, int raceID, bool timeout)
+{
+ int host = GetRaceHost(raceID);
+
+ if (timeout)
+ {
+ switch (GetRaceInfo(raceID, RaceInfo_Type))
+ {
+ case RaceType_Normal:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Request Not Accepted In Time (Target)");
+ GOKZ_PrintToChat(host, true, "%t", "Race Request Not Accepted In Time (Host)", client);
+ }
+ case RaceType_Duel:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Duel Request Not Accepted In Time (Target)");
+ GOKZ_PrintToChat(host, true, "%t", "Duel Request Not Accepted In Time (Host)", client);
+ }
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Have Declined");
+ GOKZ_PrintToChat(host, true, "%t", "Player Has Declined", client);
+ }
+}
+
+void OnRaceStarted_Announce(int raceID)
+{
+ if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Normal)
+ {
+ PrintToChatAllInRace(raceID, true, true, "%t", "Race Host Started Countdown", GetRaceHost(raceID));
+ }
+}
+
+void OnRaceAborted_Announce(int raceID)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client) && GetRaceID(client) == raceID)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Has Been Aborted");
+ if (GetStatus(client) == RacerStatus_Racing)
+ {
+ GOKZ_PlayErrorSound(client);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/api.sp b/sourcemod/scripting/gokz-racing/api.sp
new file mode 100644
index 0000000..13d82a3
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/api.sp
@@ -0,0 +1,107 @@
+static GlobalForward H_OnFinish;
+static GlobalForward H_OnSurrender;
+static GlobalForward H_OnRequestReceived;
+static GlobalForward H_OnRequestAccepted;
+static GlobalForward H_OnRequestDeclined;
+static GlobalForward H_OnRaceRegistered;
+static GlobalForward H_OnRaceInfoChanged;
+
+
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnFinish = new GlobalForward("GOKZ_RC_OnFinish", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
+ H_OnSurrender = new GlobalForward("GOKZ_RC_OnSurrender", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnRequestReceived = new GlobalForward("GOKZ_RC_OnRequestReceived", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnRequestAccepted = new GlobalForward("GOKZ_RC_OnRequestAccepted", ET_Ignore, Param_Cell, Param_Cell);
+ H_OnRequestDeclined = new GlobalForward("GOKZ_RC_OnRequestDeclined", ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
+ H_OnRaceRegistered = new GlobalForward("GOKZ_RC_OnRaceRegistered", ET_Ignore, Param_Cell);
+ H_OnRaceInfoChanged = new GlobalForward("GOKZ_RC_OnRaceInfoChanged", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell);
+}
+
+void Call_OnFinish(int client, int raceID, int place)
+{
+ Call_StartForward(H_OnFinish);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_PushCell(place);
+ Call_Finish();
+}
+
+void Call_OnSurrender(int client, int raceID)
+{
+ Call_StartForward(H_OnSurrender);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRequestReceived(int client, int raceID)
+{
+ Call_StartForward(H_OnRequestReceived);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRequestAccepted(int client, int raceID)
+{
+ Call_StartForward(H_OnRequestAccepted);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRequestDeclined(int client, int raceID, bool timeout)
+{
+ Call_StartForward(H_OnRequestDeclined);
+ Call_PushCell(client);
+ Call_PushCell(raceID);
+ Call_PushCell(timeout);
+ Call_Finish();
+}
+
+void Call_OnRaceRegistered(int raceID)
+{
+ Call_StartForward(H_OnRaceRegistered);
+ Call_PushCell(raceID);
+ Call_Finish();
+}
+
+void Call_OnRaceInfoChanged(int raceID, RaceInfo infoIndex, int oldValue, int newValue)
+{
+ Call_StartForward(H_OnRaceInfoChanged);
+ Call_PushCell(raceID);
+ Call_PushCell(infoIndex);
+ Call_PushCell(oldValue);
+ Call_PushCell(newValue);
+ Call_Finish();
+}
+
+
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_RC_GetRaceInfo", Native_GetRaceInfo);
+ CreateNative("GOKZ_RC_GetStatus", Native_GetStatus);
+ CreateNative("GOKZ_RC_GetRaceID", Native_GetRaceID);
+}
+
+public int Native_GetRaceInfo(Handle plugin, int numParams)
+{
+ return GetRaceInfo(GetNativeCell(1), GetNativeCell(2));
+}
+
+public int Native_GetStatus(Handle plugin, int numParams)
+{
+ return GetStatus(GetNativeCell(1));
+}
+
+public int Native_GetRaceID(Handle plugin, int numParams)
+{
+ return GetRaceID(GetNativeCell(1));
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/commands.sp b/sourcemod/scripting/gokz-racing/commands.sp
new file mode 100644
index 0000000..9fbd7ab
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/commands.sp
@@ -0,0 +1,47 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_accept", CommandAccept, "[KZ] Accept an incoming race request.");
+ RegConsoleCmd("sm_decline", CommandDecline, "[KZ] Decline an incoming race request.");
+ RegConsoleCmd("sm_surrender", CommandSurrender, "[KZ] Surrender your race.");
+ RegConsoleCmd("sm_duel", CommandDuel, "[KZ] Open the duel menu.");
+ RegConsoleCmd("sm_challenge", CommandDuel, "[KZ] Open the duel menu.");
+ RegConsoleCmd("sm_abort", CommandAbort, "[KZ] Abort the race you are hosting.");
+
+ RegAdminCmd("sm_race", CommandRace, ADMFLAG_RESERVATION, "[KZ] Open the race hosting menu.");
+}
+
+public Action CommandAccept(int client, int args)
+{
+ AcceptRequest(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDecline(int client, int args)
+{
+ DeclineRequest(client);
+ return Plugin_Handled;
+}
+
+public Action CommandSurrender(int client, int args)
+{
+ SurrenderRacer(client);
+ return Plugin_Handled;
+}
+
+public Action CommandDuel(int client, int args)
+{
+ DisplayDuelMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandAbort(int client, int args)
+{
+ AbortHostedRace(client);
+ return Plugin_Handled;
+}
+
+public Action CommandRace(int client, int args)
+{
+ DisplayRaceMenu(client);
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/duel_menu.sp b/sourcemod/scripting/gokz-racing/duel_menu.sp
new file mode 100644
index 0000000..44c519f
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/duel_menu.sp
@@ -0,0 +1,534 @@
+/*
+ A menu for initiating 1v1 races.
+*/
+
+
+
+#define ITEM_INFO_CHALLENGE "ch"
+#define ITEM_INFO_ABORT "ab"
+#define ITEM_INFO_MODE "md"
+#define ITEM_INFO_COURSE "co"
+#define ITEM_INFO_TELEPORT "tp"
+
+static int duelMenuMode[MAXPLAYERS + 1];
+static int duelMenuCourse[MAXPLAYERS + 1];
+static int duelMenuCheckpointLimit[MAXPLAYERS + 1];
+static int duelMenuCheckpointCooldown[MAXPLAYERS + 1];
+
+
+
+// =====[ PICK MODE ]=====
+
+void DisplayDuelMenu(int client, bool reset = true)
+{
+ if (InRace(client) && (!IsRaceHost(client) || GetRaceInfo(GetRaceID(client), RaceInfo_Type) != RaceType_Duel))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Already Part Of A Race");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ if (reset)
+ {
+ duelMenuMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ }
+
+ Menu menu = new Menu(MenuHandler_Duel);
+ menu.SetTitle("%T", "Duel Menu - Title", client);
+ DuelMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_Duel(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_CHALLENGE, false))
+ {
+ if (!DisplayDuelOpponentMenu(param1))
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ }
+ else if (StrEqual(info, ITEM_INFO_ABORT, false))
+ {
+ AbortHostedRace(param1);
+ DisplayDuelMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_MODE, false))
+ {
+ DisplayDuelModeMenu(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_COURSE, false))
+ {
+ int course = duelMenuCourse[param1];
+ do
+ {
+ course++;
+ if (!GOKZ_IsValidCourse(course))
+ {
+ course = 0;
+ }
+ } while (!GOKZ_GetCourseRegistered(course) && course != duelMenuCourse[param1]);
+ duelMenuCourse[param1] = course;
+ DisplayDuelMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_TELEPORT, false))
+ {
+ DisplayDuelCheckpointMenu(param1);
+ }
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void DuelMenuAddItems(int client, Menu menu)
+{
+ char display[64];
+
+ menu.RemoveAllItems();
+
+ FormatEx(display, sizeof(display), "%T", "Duel Menu - Choose Opponent", client);
+ menu.AddItem(ITEM_INFO_CHALLENGE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ FormatEx(display, sizeof(display), "%T\n \n%T", "Race Menu - Abort Race", client, "Race Menu - Rules", client);
+ menu.AddItem(ITEM_INFO_ABORT, display, InRace(client) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%s", gC_ModeNames[duelMenuMode[client]]);
+ menu.AddItem(ITEM_INFO_MODE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ if (duelMenuCourse[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Race Rules - Main Course", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T %d", "Race Rules - Bonus Course", client, duelMenuCourse[client]);
+ }
+ menu.AddItem(ITEM_INFO_COURSE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ FormatEx(display, sizeof(display), "%s", GetDuelRuleSummary(client, duelMenuCheckpointLimit[client], duelMenuCheckpointCooldown[client]));
+ menu.AddItem(ITEM_INFO_TELEPORT, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+}
+
+
+
+// =====[ MODE MENU ]=====
+
+static void DisplayDuelModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_DuelMode);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Mode Rule Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, true);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ duelMenuMode[param1] = param2;
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CHECKPOINT MENU ]=====
+
+static void DisplayDuelCheckpointMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_DuelCheckpoint);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Checkpoint Rule Menu - Title", client);
+ DuelCheckpointMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelCheckpoint(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ switch (param2)
+ {
+ case CheckpointRule_None:
+ {
+ duelMenuCheckpointCooldown[param1] = 0;
+ duelMenuCheckpointLimit[param1] = 0;
+ DisplayDuelMenu(param1, false);
+ }
+ case CheckpointRule_Limit:
+ {
+ DisplayCheckpointLimitMenu(param1);
+ }
+ case CheckpointRule_Cooldown:
+ {
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ case CheckpointRule_Unlimited:
+ {
+ duelMenuCheckpointCooldown[param1] = 0;
+ duelMenuCheckpointLimit[param1] = -1;
+ DisplayDuelMenu(param1, false);
+ }
+ }
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void DuelCheckpointMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - None", client);
+ menu.AddItem("", display);
+
+ if (duelMenuCheckpointLimit[client] == -1)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Limit", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Limit", client, duelMenuCheckpointLimit[client]);
+ }
+ menu.AddItem("", display);
+
+ if (duelMenuCheckpointCooldown[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Cooldown", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Cooldown", client, duelMenuCheckpointCooldown[client]);
+ }
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Unlimited", client);
+ menu.AddItem("", display);
+}
+
+
+
+// =====[ CP LIMIT MENU ]=====
+
+static void DisplayCheckpointLimitMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_DuelCheckpointLimit);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (duelMenuCheckpointLimit[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Unlimited", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Limited", client, duelMenuCheckpointLimit[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add One", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add Five", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove One", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove Five", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Unlimited", client);
+ menu.AddItem("Unlimited", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelCheckpointLimit(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (duelMenuCheckpointLimit[param1] == -1)
+ {
+ duelMenuCheckpointLimit[param1]++;
+ }
+ duelMenuCheckpointLimit[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (duelMenuCheckpointLimit[param1] == -1)
+ {
+ duelMenuCheckpointLimit[param1]++;
+ }
+ duelMenuCheckpointLimit[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ duelMenuCheckpointLimit[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ duelMenuCheckpointLimit[param1] -= 5;
+ }
+ if (StrEqual(item, "Unlimited"))
+ {
+ duelMenuCheckpointLimit[param1] = -1;
+ DisplayDuelCheckpointMenu(param1);
+ return 0;
+ }
+
+ duelMenuCheckpointLimit[param1] = duelMenuCheckpointLimit[param1] < 0 ? 0 : duelMenuCheckpointLimit[param1];
+ DisplayCheckpointLimitMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CP COOLDOWN MENU ]=====
+
+static void DisplayCheckpointCooldownMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_DuelCPCooldown);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (duelMenuCheckpointCooldown[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title None", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title Limited", client, duelMenuCheckpointCooldown[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add One Second", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add Five Seconds", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove One Second", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove Five Seconds", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - None", client);
+ menu.AddItem("None", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_DuelCPCooldown(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (duelMenuCheckpointCooldown[param1] == -1)
+ {
+ duelMenuCheckpointCooldown[param1]++;
+ }
+ duelMenuCheckpointCooldown[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (duelMenuCheckpointCooldown[param1] == -1)
+ {
+ duelMenuCheckpointCooldown[param1]++;
+ }
+ duelMenuCheckpointCooldown[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ duelMenuCheckpointCooldown[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ duelMenuCheckpointCooldown[param1] -= 5;
+ }
+ if (StrEqual(item, "None"))
+ {
+ duelMenuCheckpointCooldown[param1] = 0;
+ DisplayDuelCheckpointMenu(param1);
+ return 0;
+ }
+
+ duelMenuCheckpointCooldown[param1] = duelMenuCheckpointCooldown[param1] < 0 ? 0 : duelMenuCheckpointCooldown[param1];
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ OPPONENT MENU ]=====
+
+static bool DisplayDuelOpponentMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_DuelOpponent);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Duel Opponent Selection Menu - Title", client);
+ if (DuelOpponentMenuAddItems(client, menu) == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Opponents Available");
+ GOKZ_PlayErrorSound(client);
+ delete menu;
+ return false;
+ }
+ menu.Display(client, MENU_TIME_FOREVER);
+
+ return true;
+}
+
+static int DuelOpponentMenuAddItems(int client, Menu menu)
+{
+ int count = 0;
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ char display[MAX_NAME_LENGTH];
+ if (i != client && IsClientInGame(i) && !IsFakeClient(i) && !InRace(i))
+ {
+ FormatEx(display, sizeof(display), "%N", i);
+ menu.AddItem(IntToStringEx(GetClientUserId(i)), display);
+ count++;
+ }
+ }
+ return count;
+}
+
+public int MenuHandler_DuelOpponent(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+ int target = GetClientOfUserId(StringToInt(info));
+ if (IsValidClient(target))
+ {
+ if (SendDuelRequest(param1, target))
+ {
+ GOKZ_PrintToChat(param1, true, "%t", "Duel Request Sent", target);
+ }
+ else
+ {
+ DisplayDuelOpponentMenu(param1);
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid");
+ GOKZ_PlayErrorSound(param1);
+ DisplayDuelOpponentMenu(param1);
+ }
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayDuelMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+static bool SendDuelRequest(int host, int target)
+{
+ if (InRace(target))
+ {
+ GOKZ_PrintToChat(host, true, "%t", "Player Already In A Race", target);
+ GOKZ_PlayErrorSound(host);
+ return false;
+ }
+
+ HostRace(host, RaceType_Duel, duelMenuCourse[host], duelMenuMode[host], duelMenuCheckpointLimit[host], duelMenuCheckpointCooldown[host]);
+ return SendRequest(host, target);
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+char[] GetDuelRuleSummary(int client, int checkpointLimit, int checkpointCooldown)
+{
+ char rulesString[64];
+ if (checkpointLimit == -1 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Unlimited", client);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Checkpoints", client, checkpointLimit);
+ }
+ else if (checkpointLimit == -1 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Cooldown", client, checkpointCooldown);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Everything", client, checkpointLimit, checkpointCooldown);
+ }
+ else if (checkpointLimit == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - No Checkpoints", client);
+ }
+
+ return rulesString;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/race.sp b/sourcemod/scripting/gokz-racing/race.sp
new file mode 100644
index 0000000..5ddc2a8
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/race.sp
@@ -0,0 +1,221 @@
+/*
+ Race info storing and accessing using a StringMap.
+ Each race is given a unique race ID when created.
+ See the RaceInfo enum for what information is accessible.
+ See the RaceStatus enum for possible race states.
+*/
+
+
+
+static StringMap raceInfo;
+static int lastRaceID;
+
+
+
+// =====[ GENERAL ]=====
+
+int GetRaceInfo(int raceID, RaceInfo prop)
+{
+ ArrayList info;
+ if (raceInfo.GetValue(IntToStringEx(raceID), info))
+ {
+ return info.Get(view_as<int>(prop));
+ }
+ else
+ {
+ return -1;
+ }
+}
+
+static bool SetRaceInfo(int raceID, RaceInfo prop, int value)
+{
+ ArrayList info;
+ if (raceInfo.GetValue(IntToStringEx(raceID), info))
+ {
+ int oldValue = info.Get(view_as<int>(prop));
+ if (oldValue != value)
+ {
+ info.Set(view_as<int>(prop), value);
+ Call_OnRaceInfoChanged(raceID, prop, oldValue, value);
+ }
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+int IncrementFinishedRacerCount(int raceID)
+{
+ int finishedRacers = GetRaceInfo(raceID, RaceInfo_FinishedRacerCount) + 1;
+ SetRaceInfo(raceID, RaceInfo_FinishedRacerCount, finishedRacers);
+ return finishedRacers;
+}
+
+int GetRaceHost(int raceID)
+{
+ return GetClientOfUserId(GetRaceInfo(raceID, RaceInfo_HostUserID));
+}
+
+ArrayList GetUnfinishedRacers(int raceID)
+{
+ ArrayList racers = new ArrayList();
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (GetRaceID(i) == raceID && !IsFinished(i))
+ {
+ racers.Push(i);
+ }
+ }
+ return racers;
+}
+
+int GetUnfinishedRacersCount(int raceID)
+{
+ ArrayList racers = GetUnfinishedRacers(raceID);
+ int count = racers.Length;
+ delete racers;
+ return count;
+}
+
+ArrayList GetAcceptedRacers(int raceID)
+{
+ ArrayList racers = new ArrayList();
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (GetRaceID(i) == raceID && IsAccepted(i))
+ {
+ racers.Push(i);
+ }
+ }
+ return racers;
+}
+
+int GetAcceptedRacersCount(int raceID)
+{
+ ArrayList racers = GetAcceptedRacers(raceID);
+ int count = racers.Length;
+ delete racers;
+ return count;
+}
+
+
+
+// =====[ REGISTRATION ]=====
+
+int RegisterRace(int host, int type, int course, int mode, int checkpointRule, int cooldownRule)
+{
+ int raceID = ++lastRaceID;
+
+ ArrayList info = new ArrayList(1, view_as<int>(RACEINFO_COUNT));
+ info.Set(view_as<int>(RaceInfo_ID), raceID);
+ info.Set(view_as<int>(RaceInfo_Status), RaceStatus_Pending);
+ info.Set(view_as<int>(RaceInfo_HostUserID), GetClientUserId(host));
+ info.Set(view_as<int>(RaceInfo_FinishedRacerCount), 0);
+ info.Set(view_as<int>(RaceInfo_Type), type);
+ info.Set(view_as<int>(RaceInfo_Course), course);
+ info.Set(view_as<int>(RaceInfo_Mode), mode);
+ info.Set(view_as<int>(RaceInfo_CheckpointRule), checkpointRule);
+ info.Set(view_as<int>(RaceInfo_CooldownRule), cooldownRule);
+
+ raceInfo.SetValue(IntToStringEx(raceID), info);
+
+ Call_OnRaceRegistered(raceID);
+
+ return raceID;
+}
+
+static void UnregisterRace(int raceID)
+{
+ ArrayList info;
+ if (raceInfo.GetValue(IntToStringEx(raceID), info))
+ {
+ delete info;
+ raceInfo.Remove(IntToStringEx(raceID));
+ }
+}
+
+
+
+// =====[ START ]=====
+
+bool StartRace(int raceID)
+{
+ SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Countdown);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GetRaceID(client) == raceID)
+ {
+ StartRacer(client);
+ GOKZ_PrintToChat(client, true, "%t", "Race Countdown Started");
+ }
+ }
+
+ CreateTimer(RC_COUNTDOWN_TIME, Timer_EndCountdown, raceID);
+
+ return true;
+}
+
+public Action Timer_EndCountdown(Handle timer, int raceID)
+{
+ SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Started);
+ return Plugin_Continue;
+}
+
+
+
+// =====[ ABORT ]=====
+
+bool AbortRace(int raceID)
+{
+ SetRaceInfo(raceID, RaceInfo_Status, RaceStatus_Aborting);
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (GetRaceID(client) == raceID)
+ {
+ AbortRacer(client);
+ GOKZ_PrintToChat(client, true, "%t", "Race Has Been Aborted");
+ GOKZ_PlayErrorSound(client);
+ }
+ }
+
+ UnregisterRace(raceID);
+
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnPluginStart_Race()
+{
+ raceInfo = new StringMap();
+}
+
+void OnFinish_Race(int raceID)
+{
+ if (GetUnfinishedRacersCount(raceID) == 0)
+ {
+ UnregisterRace(raceID);
+ }
+}
+
+void OnRequestAccepted_Race(int raceID)
+{
+ if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Duel)
+ {
+ StartRace(raceID);
+ }
+}
+
+void OnRequestDeclined_Race(int raceID)
+{
+ if (GetRaceInfo(raceID, RaceInfo_Type) == RaceType_Duel)
+ {
+ AbortRace(raceID);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/race_menu.sp b/sourcemod/scripting/gokz-racing/race_menu.sp
new file mode 100644
index 0000000..0518a2a
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/race_menu.sp
@@ -0,0 +1,464 @@
+/*
+ A menu for hosting big races.
+*/
+
+
+
+#define ITEM_INFO_START "st"
+#define ITEM_INFO_ABORT "ab"
+#define ITEM_INFO_INVITE "iv"
+#define ITEM_INFO_MODE "md"
+#define ITEM_INFO_COURSE "co"
+#define ITEM_INFO_TELEPORT "tp"
+
+static int raceMenuMode[MAXPLAYERS + 1];
+static int raceMenuCourse[MAXPLAYERS + 1];
+static int raceMenuCheckpointLimit[MAXPLAYERS + 1];
+static int raceMenuCheckpointCooldown[MAXPLAYERS + 1];
+
+
+
+// =====[ PICK MODE ]=====
+
+void DisplayRaceMenu(int client, bool reset = true)
+{
+ if (InRace(client) && (!IsRaceHost(client) || GetRaceInfo(GetRaceID(client), RaceInfo_Type) != RaceType_Normal))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Already Part Of A Race");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ if (reset)
+ {
+ raceMenuMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ raceMenuCourse[client] = 0;
+ }
+
+ Menu menu = new Menu(MenuHandler_Race);
+ menu.SetTitle("%T", "Race Menu - Title", client);
+ RaceMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_Race(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+
+ if (StrEqual(info, ITEM_INFO_START, false))
+ {
+ if (!StartHostedRace(param1))
+ {
+ DisplayRaceMenu(param1, false);
+ }
+ }
+ else if (StrEqual(info, ITEM_INFO_ABORT, false))
+ {
+ AbortHostedRace(param1);
+ DisplayRaceMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_INVITE, false))
+ {
+ if (!InRace(param1))
+ {
+ HostRace(param1, RaceType_Normal, raceMenuCourse[param1], raceMenuMode[param1], raceMenuCheckpointLimit[param1], raceMenuCheckpointCooldown[param1]);
+ }
+
+ SendRequestAll(param1);
+ GOKZ_PrintToChat(param1, true, "%t", "You Invited Everyone");
+ DisplayRaceMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_MODE, false))
+ {
+ DisplayRaceModeMenu(param1);
+ }
+ else if (StrEqual(info, ITEM_INFO_COURSE, false))
+ {
+ int course = raceMenuCourse[param1];
+ do
+ {
+ course++;
+ if (!GOKZ_IsValidCourse(course))
+ {
+ course = 0;
+ }
+ } while (!GOKZ_GetCourseRegistered(course) && course != raceMenuCourse[param1]);
+ raceMenuCourse[param1] = course;
+ DisplayRaceMenu(param1, false);
+ }
+ else if (StrEqual(info, ITEM_INFO_TELEPORT, false))
+ {
+ DisplayRaceCheckpointMenu(param1);
+ }
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void RaceMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ bool pending = GetRaceInfo(GetRaceID(client), RaceInfo_Status) == RaceStatus_Pending;
+ FormatEx(display, sizeof(display), "%T", "Race Menu - Start Race", client);
+ menu.AddItem(ITEM_INFO_START, display, (InRace(client) && pending) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%T", "Race Menu - Invite Everyone", client);
+ menu.AddItem(ITEM_INFO_INVITE, display, (!InRace(client) || pending) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%T\n \n%T", "Race Menu - Abort Race", client, "Race Menu - Rules", client);
+ menu.AddItem(ITEM_INFO_ABORT, display, InRace(client) ? ITEMDRAW_DEFAULT : ITEMDRAW_DISABLED);
+
+ FormatEx(display, sizeof(display), "%s", gC_ModeNames[raceMenuMode[client]]);
+ menu.AddItem(ITEM_INFO_MODE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ if (raceMenuCourse[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Race Rules - Main Course", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T %d", "Race Rules - Bonus Course", client, raceMenuCourse[client]);
+ }
+ menu.AddItem(ITEM_INFO_COURSE, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+
+ FormatEx(display, sizeof(display), "%s", GetRaceRuleSummary(client, raceMenuCheckpointLimit[client], raceMenuCheckpointCooldown[client]));
+ menu.AddItem(ITEM_INFO_TELEPORT, display, InRace(client) ? ITEMDRAW_DISABLED : ITEMDRAW_DEFAULT);
+}
+
+
+
+// =====[ MODE MENU ]=====
+
+static void DisplayRaceModeMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_RaceMode);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Mode Rule Menu - Title", client);
+ GOKZ_MenuAddModeItems(client, menu, true);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ raceMenuMode[param1] = param2;
+ DisplayRaceMenu(param1, false);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CHECKPOINT MENU ]=====
+
+static void DisplayRaceCheckpointMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_RaceCheckpoint);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+ menu.SetTitle("%T", "Checkpoint Rule Menu - Title", client);
+ RaceCheckpointMenuAddItems(client, menu);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceCheckpoint(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ switch (param2)
+ {
+ case CheckpointRule_None:
+ {
+ raceMenuCheckpointLimit[param1] = 0;
+ raceMenuCheckpointCooldown[param1] = 0;
+ DisplayRaceMenu(param1, false);
+ }
+ case CheckpointRule_Limit:
+ {
+ DisplayCheckpointLimitMenu(param1);
+ }
+ case CheckpointRule_Cooldown:
+ {
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ case CheckpointRule_Unlimited:
+ {
+ raceMenuCheckpointLimit[param1] = -1;
+ raceMenuCheckpointCooldown[param1] = 0;
+ DisplayRaceMenu(param1, false);
+ }
+ }
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceMenu(param1, false);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+void RaceCheckpointMenuAddItems(int client, Menu menu)
+{
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - None", client);
+ menu.AddItem("", display);
+
+ if (raceMenuCheckpointLimit[client] == -1)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Limit", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Limit", client, raceMenuCheckpointLimit[client]);
+ }
+ menu.AddItem("", display);
+
+ if (raceMenuCheckpointCooldown[client] == 0)
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - No Checkpoint Cooldown", client);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Checkpoint Cooldown", client, raceMenuCheckpointCooldown[client]);
+ }
+ menu.AddItem("", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Rule - Unlimited", client);
+ menu.AddItem("", display);
+}
+
+
+
+// =====[ CP LIMIT MENU ]=====
+
+static void DisplayCheckpointLimitMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_RaceCheckpointLimit);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (raceMenuCheckpointLimit[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Unlimited", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Limit Menu - Title Limited", client, raceMenuCheckpointLimit[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add One", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Add Five", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove One", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Remove Five", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Limit Menu - Unlimited", client);
+ menu.AddItem("Unlimited", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceCheckpointLimit(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (raceMenuCheckpointLimit[param1] == -1)
+ {
+ raceMenuCheckpointLimit[param1]++;
+ }
+ raceMenuCheckpointLimit[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (raceMenuCheckpointLimit[param1] == -1)
+ {
+ raceMenuCheckpointLimit[param1]++;
+ }
+ raceMenuCheckpointLimit[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ raceMenuCheckpointLimit[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ raceMenuCheckpointLimit[param1] -= 5;
+ }
+ if (StrEqual(item, "Unlimited"))
+ {
+ raceMenuCheckpointLimit[param1] = -1;
+ DisplayRaceCheckpointMenu(param1);
+ return 0;
+ }
+
+ raceMenuCheckpointLimit[param1] = raceMenuCheckpointLimit[param1] < 0 ? 0 : raceMenuCheckpointLimit[param1];
+ DisplayCheckpointLimitMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ CP COOLDOWN MENU ]=====
+
+static void DisplayCheckpointCooldownMenu(int client)
+{
+ char display[32];
+
+ Menu menu = new Menu(MenuHandler_RaceCPCooldown);
+ menu.ExitButton = false;
+ menu.ExitBackButton = true;
+
+ if (raceMenuCheckpointCooldown[client] == -1)
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title None", client);
+ }
+ else
+ {
+ menu.SetTitle("%T", "Checkpoint Cooldown Menu - Title Limited", client, raceMenuCheckpointCooldown[client]);
+ }
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add One Second", client);
+ menu.AddItem("+1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Add Five Seconds", client);
+ menu.AddItem("+5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove One Second", client);
+ menu.AddItem("-1", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - Remove Five Seconds", client);
+ menu.AddItem("-5", display);
+
+ FormatEx(display, sizeof(display), "%T", "Checkpoint Cooldown Menu - None", client);
+ menu.AddItem("None", display);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+public int MenuHandler_RaceCPCooldown(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char item[32];
+ menu.GetItem(param2, item, sizeof(item));
+ if (StrEqual(item, "+1"))
+ {
+ if (raceMenuCheckpointCooldown[param1] == -1)
+ {
+ raceMenuCheckpointCooldown[param1]++;
+ }
+ raceMenuCheckpointCooldown[param1]++;
+ }
+ if (StrEqual(item, "+5"))
+ {
+ if (raceMenuCheckpointCooldown[param1] == -1)
+ {
+ raceMenuCheckpointCooldown[param1]++;
+ }
+ raceMenuCheckpointCooldown[param1] += 5;
+ }
+ if (StrEqual(item, "-1"))
+ {
+ raceMenuCheckpointCooldown[param1]--;
+ }
+ if (StrEqual(item, "-5"))
+ {
+ raceMenuCheckpointCooldown[param1] -= 5;
+ }
+ if (StrEqual(item, "None"))
+ {
+ raceMenuCheckpointCooldown[param1] = 0;
+ DisplayRaceCheckpointMenu(param1);
+ return 0;
+ }
+
+ raceMenuCheckpointCooldown[param1] = raceMenuCheckpointCooldown[param1] < 0 ? 0 : raceMenuCheckpointCooldown[param1];
+ DisplayCheckpointCooldownMenu(param1);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayRaceCheckpointMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+char[] GetRaceRuleSummary(int client, int checkpointLimit, int checkpointCooldown)
+{
+ char rulesString[64];
+ if (checkpointLimit == -1 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Unlimited", client);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Checkpoints", client, checkpointLimit);
+ }
+ else if (checkpointLimit == -1 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Cooldown", client, checkpointCooldown);
+ }
+ else if (checkpointLimit > 0 && checkpointCooldown > 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - Limited Everything", client, checkpointLimit, checkpointCooldown);
+ }
+ else if (checkpointLimit == 0)
+ {
+ FormatEx(rulesString, sizeof(rulesString), "%T", "Rule Summary - No Checkpoints", client);
+ }
+
+ return rulesString;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-racing/racer.sp b/sourcemod/scripting/gokz-racing/racer.sp
new file mode 100644
index 0000000..eb4ff3f
--- /dev/null
+++ b/sourcemod/scripting/gokz-racing/racer.sp
@@ -0,0 +1,439 @@
+/*
+ Functions that affect the state of clients participating in a race.
+ See the RacerStatus enum for possible states.
+*/
+
+
+
+static int racerStatus[MAXPLAYERS + 1];
+static int racerRaceID[MAXPLAYERS + 1];
+static float lastTimerStartTime[MAXPLAYERS + 1];
+static float lastCheckpointTime[MAXPLAYERS + 1];
+
+
+
+// =====[ EVENTS ]=====
+
+Action OnTimerStart_Racer(int client, int course)
+{
+ if (InCountdown(client)
+ || InStartedRace(client) && (!InRaceMode(client) || !IsRaceCourse(client, course)))
+ {
+ return Plugin_Stop;
+ }
+
+ return Plugin_Continue;
+}
+
+Action OnTimerStart_Post_Racer(int client)
+{
+ lastTimerStartTime[client] = GetGameTime();
+ return Plugin_Continue;
+}
+
+Action OnMakeCheckpoint_Racer(int client)
+{
+ if (GOKZ_GetTimerRunning(client) && InStartedRace(client))
+ {
+ int checkpointRule = GetRaceInfo(GetRaceID(client), RaceInfo_CheckpointRule);
+ if (checkpointRule == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Checkpoints Not Allowed During Race");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ else if (checkpointRule != -1 && GOKZ_GetCheckpointCount(client) >= checkpointRule)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Checkpoints Left");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+
+ float cooldownRule = float(GetRaceInfo(GetRaceID(client), RaceInfo_CooldownRule));
+ float timeSinceLastCheckpoint = FloatMin(
+ GetGameTime() - lastTimerStartTime[client],
+ GetGameTime() - lastCheckpointTime[client]);
+ if (timeSinceLastCheckpoint < cooldownRule)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Checkpoint On Cooldown", (cooldownRule - timeSinceLastCheckpoint));
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ }
+
+ return Plugin_Continue;
+}
+
+void OnMakeCheckpoint_Post_Racer(int client)
+{
+ lastCheckpointTime[client] = GetGameTime();
+}
+
+Action OnUndoTeleport_Racer(int client)
+{
+ if (GOKZ_GetTimerRunning(client)
+ && InStartedRace(client)
+ && GetRaceInfo(GetRaceID(client), RaceInfo_CheckpointRule) != -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Undo TP Not Allowed During Race");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+
+ return Plugin_Continue;
+}
+
+
+
+// =====[ GENERAL ]=====
+
+int GetStatus(int client)
+{
+ return racerStatus[client];
+}
+
+int GetRaceID(int client)
+{
+ return racerRaceID[client];
+}
+
+bool InRace(int client)
+{
+ return GetStatus(client) != RacerStatus_Available;
+}
+
+bool InStartedRace(int client)
+{
+ return GetStatus(client) == RacerStatus_Racing;
+}
+
+bool InCountdown(int client)
+{
+ return GetRaceInfo(GetRaceID(client), RaceInfo_Status) == RaceStatus_Countdown;
+}
+
+bool InRaceMode(int client)
+{
+ return GOKZ_GetCoreOption(client, Option_Mode) == GetRaceInfo(GetRaceID(client), RaceInfo_Mode);
+}
+
+bool IsRaceCourse(int client, int course)
+{
+ return course == GetRaceInfo(GetRaceID(client), RaceInfo_Course);
+}
+
+bool IsFinished(int client)
+{
+ int status = GetStatus(client);
+ return status == RacerStatus_Finished || status == RacerStatus_Surrendered;
+}
+
+bool IsAccepted(int client)
+{
+ return GetStatus(client) == RacerStatus_Accepted;
+}
+
+bool IsRaceHost(int client)
+{
+ return GetRaceHost(GetRaceID(client)) == client;
+}
+
+static void ResetRacer(int client)
+{
+ racerStatus[client] = RacerStatus_Available;
+ racerRaceID[client] = -1;
+ lastTimerStartTime[client] = 0.0;
+ lastCheckpointTime[client] = 0.0;
+}
+
+static void ResetRacersInRace(int raceID)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (racerRaceID[client] == raceID)
+ {
+ ResetRacer(client);
+ }
+ }
+}
+
+
+
+// =====[ RACING ]=====
+
+void StartRacer(int client)
+{
+ if (racerStatus[client] == RacerStatus_Pending)
+ {
+ DeclineRequest(client, true);
+ return;
+ }
+
+ if (racerStatus[client] != RacerStatus_Accepted)
+ {
+ return;
+ }
+
+ racerStatus[client] = RacerStatus_Racing;
+
+ // Prepare the racer
+ GOKZ_StopTimer(client);
+ GOKZ_SetCoreOption(client, Option_Mode, GetRaceInfo(racerRaceID[client], RaceInfo_Mode));
+
+ int course = GetRaceInfo(racerRaceID[client], RaceInfo_Course);
+ if (GOKZ_SetStartPositionToMapStart(client, course))
+ {
+ GOKZ_TeleportToStart(client);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Start Found", course);
+ }
+}
+
+bool FinishRacer(int client, int course)
+{
+ if (racerStatus[client] != RacerStatus_Racing ||
+ course != GetRaceInfo(racerRaceID[client], RaceInfo_Course))
+ {
+ return false;
+ }
+
+ racerStatus[client] = RacerStatus_Finished;
+
+ int raceID = racerRaceID[client];
+ int place = IncrementFinishedRacerCount(raceID);
+
+ Call_OnFinish(client, raceID, place);
+
+ CheckRaceFinished(raceID);
+
+ return true;
+}
+
+bool SurrenderRacer(int client)
+{
+ if (racerStatus[client] == RacerStatus_Available
+ || racerStatus[client] == RacerStatus_Surrendered)
+ {
+ return false;
+ }
+
+ racerStatus[client] = RacerStatus_Surrendered;
+
+ int raceID = racerRaceID[client];
+
+ Call_OnSurrender(client, raceID);
+
+ CheckRaceFinished(raceID);
+
+ return true;
+}
+
+// Auto-finish last remaining racer, and reset everyone if no one is left
+static void CheckRaceFinished(int raceID)
+{
+ ArrayList remainingRacers = GetUnfinishedRacers(raceID);
+ if (remainingRacers.Length == 1)
+ {
+ int lastRacer = remainingRacers.Get(0);
+ FinishRacer(lastRacer, GetRaceInfo(racerRaceID[lastRacer], RaceInfo_Course));
+ }
+ else if (remainingRacers.Length == 0)
+ {
+ ResetRacersInRace(raceID);
+ }
+ delete remainingRacers;
+}
+
+bool AbortRacer(int client)
+{
+ if (racerStatus[client] == RacerStatus_Available)
+ {
+ return false;
+ }
+
+ ResetRacer(client);
+
+ return true;
+}
+
+
+
+// =====[ HOSTING ]=====
+
+int HostRace(int client, int type, int course, int mode, int checkpointRule, int cooldownRule)
+{
+ if (InRace(client))
+ {
+ return -1;
+ }
+
+ int raceID = RegisterRace(client, type, course, mode, checkpointRule, cooldownRule);
+ racerRaceID[client] = raceID;
+ racerStatus[client] = RacerStatus_Accepted;
+
+ return raceID;
+}
+
+bool StartHostedRace(int client)
+{
+ if (!InRace(client) || !IsRaceHost(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Not Hosting A Race");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ int raceID = racerRaceID[client];
+
+ if (GetRaceInfo(raceID, RaceInfo_Status) != RaceStatus_Pending)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Race Already Started");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ if (GetAcceptedRacersCount(raceID) <= 1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No One Accepted");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ return StartRace(raceID);
+}
+
+bool AbortHostedRace(int client)
+{
+ if (!InRace(client) || !IsRaceHost(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "You Are Not Hosting A Race");
+ GOKZ_PlayErrorSound(client);
+ return false;
+ }
+
+ int raceID = racerRaceID[client];
+
+ return AbortRace(raceID);
+}
+
+
+
+// =====[ REQUESTS ]=====
+
+bool SendRequest(int host, int target)
+{
+ if (IsFakeClient(target) || target == host || InRace(target)
+ || !IsRaceHost(host) || GetRaceInfo(racerRaceID[host], RaceInfo_Status) != RaceStatus_Pending)
+ {
+ return false;
+ }
+
+ int raceID = racerRaceID[host];
+
+ racerRaceID[target] = raceID;
+ racerStatus[target] = RacerStatus_Pending;
+
+ // Host callback
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(host));
+ data.WriteCell(GetClientUserId(target));
+ data.WriteCell(raceID);
+ CreateTimer(RC_REQUEST_TIMEOUT_TIME, Timer_RequestTimeout, data);
+
+ Call_OnRequestReceived(target, raceID);
+
+ return true;
+}
+
+public Action Timer_RequestTimeout(Handle timer, DataPack data)
+{
+ data.Reset();
+ int host = GetClientOfUserId(data.ReadCell());
+ int target = GetClientOfUserId(data.ReadCell());
+ int raceID = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(host) || racerRaceID[host] != raceID
+ || !IsValidClient(target) || racerRaceID[target] != raceID)
+ {
+ return Plugin_Continue;
+ }
+
+ // If haven't accepted by now, auto decline the race
+ if (racerStatus[target] == RacerStatus_Pending)
+ {
+ DeclineRequest(target, true);
+ }
+ return Plugin_Continue;
+}
+
+int SendRequestAll(int host)
+{
+ int sentCount = 0;
+ for (int target = 1; target <= MaxClients; target++)
+ {
+ if (IsClientInGame(target) && SendRequest(host, target))
+ {
+ sentCount++;
+ }
+ }
+ return sentCount;
+}
+
+bool AcceptRequest(int client)
+{
+ if (GetStatus(client) != RacerStatus_Pending)
+ {
+ return false;
+ }
+
+ racerStatus[client] = RacerStatus_Accepted;
+
+ Call_OnRequestAccepted(client, racerRaceID[client]);
+
+ return true;
+}
+
+bool DeclineRequest(int client, bool timeout = false)
+{
+ if (GetStatus(client) != RacerStatus_Pending)
+ {
+ return false;
+ }
+
+ int raceID = racerRaceID[client];
+ ResetRacer(client);
+
+ Call_OnRequestDeclined(client, raceID, timeout);
+
+ return true;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Racer(int client)
+{
+ ResetRacer(client);
+}
+
+void OnClientDisconnect_Racer(int client)
+{
+ // Abort if player was the host of the race, else surrender
+ if (InRace(client))
+ {
+ if (IsRaceHost(client) && GetRaceInfo(racerRaceID[client], RaceInfo_Status) == RaceStatus_Pending)
+ {
+ AbortRace(racerRaceID[client]);
+ }
+ else
+ {
+ SurrenderRacer(client);
+ }
+ }
+
+ ResetRacer(client);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-replays.sp b/sourcemod/scripting/gokz-replays.sp
new file mode 100644
index 0000000..bc826c4
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays.sp
@@ -0,0 +1,397 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdkhooks>
+#include <sdktools>
+#include <dhooks>
+
+#include <movementapi>
+
+#include <gokz/core>
+#include <gokz/localranks>
+#include <gokz/replays>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <gokz/hud>
+#include <gokz/jumpstats>
+#include <gokz/localdb>
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+//#define DEBUG
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Replays",
+ author = "DanZay",
+ description = "Records runs to disk and allows playback using bots",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-replays.txt"
+
+bool gB_GOKZHUD;
+bool gB_GOKZLocalDB;
+char gC_CurrentMap[64];
+int gI_CurrentMapFileSize;
+bool gB_HideNameChange;
+bool gB_NubRecordMissed[MAXPLAYERS + 1];
+ArrayList g_ReplayInfoCache;
+Address gA_BotDuckAddr;
+int gI_BotDuckPatchRestore[40]; // Size of patched section in gamedata
+int gI_BotDuckPatchLength;
+
+DynamicDetour gH_DHooks_TeamFull;
+
+#include "gokz-replays/commands.sp"
+#include "gokz-replays/nav.sp"
+#include "gokz-replays/playback.sp"
+#include "gokz-replays/recording.sp"
+#include "gokz-replays/replay_cache.sp"
+#include "gokz-replays/replay_menu.sp"
+#include "gokz-replays/api.sp"
+#include "gokz-replays/controls.sp"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ CreateNatives();
+ RegPluginLibrary("gokz-replays");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-replays.phrases");
+
+ CreateGlobalForwards();
+ HookEvents();
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZLocalDB = LibraryExists("gokz-localdb");
+ gB_GOKZHUD = LibraryExists("gokz-hud");
+
+ 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_GOKZHUD = gB_GOKZHUD || StrEqual(name, "gokz-hud");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZLocalDB = gB_GOKZLocalDB && !StrEqual(name, "gokz-localdb");
+ gB_GOKZHUD = gB_GOKZHUD && !StrEqual(name, "gokz-hud");
+}
+
+public void OnPluginEnd()
+{
+ // Restore bot auto duck behavior.
+ if (gA_BotDuckAddr == Address_Null)
+ {
+ return;
+ }
+ for (int i = 0; i < gI_BotDuckPatchLength; i++)
+ {
+ StoreToAddress(gA_BotDuckAddr + view_as<Address>(i), gI_BotDuckPatchRestore[i], NumberType_Int8);
+ }
+}
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ UpdateCurrentMap(); // Do first
+ OnMapStart_Nav();
+ OnMapStart_Recording();
+ OnMapStart_ReplayCache();
+}
+
+public void OnConfigsExecuted()
+{
+ FindConVar("mp_autoteambalance").BoolValue = false;
+ FindConVar("mp_limitteams").IntValue = 0;
+ // Stop the bots!
+ FindConVar("bot_stop").BoolValue = true;
+ FindConVar("bot_chatter").SetString("off");
+ FindConVar("bot_zombie").BoolValue = true;
+ FindConVar("bot_join_after_player").BoolValue = false;
+ FindConVar("bot_quota_mode").SetString("normal");
+ FindConVar("bot_quota").Flags &= ~FCVAR_NOTIFY;
+ FindConVar("bot_quota").Flags &= ~FCVAR_REPLICATED;
+}
+
+public void OnEntityCreated(int entity, const char[] classname)
+{
+ // Block trigger and door interaction for bots
+ // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer
+
+ // trigger_once | trigger_multiple.. etc
+ // func_door | func_door_rotating
+ if (StrContains(classname, "trigger_") != -1 || StrContains(classname, "_door") != -1)
+ {
+ SDKHook(entity, SDKHook_StartTouch, HookTriggers);
+ SDKHook(entity, SDKHook_EndTouch, HookTriggers);
+ SDKHook(entity, SDKHook_Touch, HookTriggers);
+ }
+}
+
+public Action HookTriggers(int entity, int other)
+{
+ if (other >= 1 && other <= MaxClients && IsFakeClient(other))
+ {
+ return Plugin_Handled;
+ }
+
+ return Plugin_Continue;
+}
+
+public Action Hook_SayText2(UserMsg msg_id, any msg, const int[] players, int playersNum, bool reliable, bool init)
+{
+ // Name change supression
+ // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer
+ if (!gB_HideNameChange)
+ {
+ return Plugin_Continue;
+ }
+
+ char msgName[24];
+ Protobuf pbmsg = msg;
+ pbmsg.ReadString("msg_name", msgName, sizeof(msgName));
+ if (StrEqual(msgName, "#Cstrike_Name_Change"))
+ {
+ gB_HideNameChange = false;
+ return Plugin_Handled;
+ }
+
+ return Plugin_Continue;
+}
+
+public MRESReturn DHooks_OnTeamFull_Pre(Address pThis, DHookReturn hReturn, DHookParam hParams)
+{
+ DHookSetReturn(hReturn, false);
+ return MRES_Supercede;
+}
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ OnClientPutInServer_Playback(client);
+ OnClientPutInServer_Recording(client);
+}
+
+public void OnClientAuthorized(int client, const char[] auth)
+{
+ OnClientAuthorized_Recording(client);
+}
+
+public void OnClientDisconnect(int client)
+{
+ OnClientDisconnect_Playback(client);
+ OnClientDisconnect_Recording(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])
+{
+ if (!IsFakeClient(client))
+ {
+ return Plugin_Continue;
+ }
+ OnPlayerRunCmd_Playback(client, buttons, vel, angles);
+ return Plugin_Changed;
+}
+
+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])
+{
+ OnPlayerRunCmdPost_Playback(client);
+ OnPlayerRunCmdPost_Recording(client, buttons, tickcount, vel, mouse);
+ OnPlayerRunCmdPost_ReplayControls(client, cmdnum);
+}
+
+public Action GOKZ_OnTimerStart(int client, int course)
+{
+ Action action = GOKZ_OnTimerStart_Recording(client);
+ if (action != Plugin_Continue)
+ {
+ return action;
+ }
+
+ return Plugin_Continue;
+}
+
+public void GOKZ_OnTimerStart_Post(int client, int course)
+{
+ gB_NubRecordMissed[client] = false;
+ GOKZ_OnTimerStart_Post_Recording(client);
+}
+
+public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ GOKZ_OnTimerEnd_Recording(client, course, time, teleportsUsed);
+}
+
+public void GOKZ_OnPause_Post(int client)
+{
+ GOKZ_OnPause_Recording(client);
+}
+
+public void GOKZ_OnResume_Post(int client)
+{
+ GOKZ_OnResume_Recording(client);
+}
+
+public void GOKZ_OnTimerStopped(int client)
+{
+ GOKZ_OnTimerStopped_Recording(client);
+}
+
+public void GOKZ_OnCountedTeleport_Post(int client)
+{
+ GOKZ_OnCountedTeleport_Recording(client);
+}
+
+public void GOKZ_LR_OnRecordMissed(int client, float recordTime, int course, int mode, int style, int recordType)
+{
+ if (recordType == RecordType_Nub)
+ {
+ gB_NubRecordMissed[client] = true;
+ }
+ GOKZ_LR_OnRecordMissed_Recording(client, recordType);
+}
+
+public void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason)
+{
+ GOKZ_AC_OnPlayerSuspected_Recording(client, reason);
+}
+
+public void GOKZ_DB_OnJumpstatPB(int client, int jumptype, int mode, float distance, int block, int strafes, float sync, float pre, float max, int airtime)
+{
+ GOKZ_DB_OnJumpstatPB_Recording(client, jumptype, distance, block, strafes, sync, pre, max, airtime);
+}
+
+public void GOKZ_OnOptionsLoaded(int client)
+{
+ if (IsFakeClient(client))
+ {
+ GOKZ_OnOptionsLoaded_Playback(client);
+ }
+}
+
+// =====[ PRIVATE ]=====
+
+static void HookEvents()
+{
+ HookUserMessage(GetUserMessageId("SayText2"), Hook_SayText2, true);
+ GameData gameData = LoadGameConfigFile("gokz-replays.games");
+
+ gH_DHooks_TeamFull = DynamicDetour.FromConf(gameData, "CCSGameRules::TeamFull");
+ if (gH_DHooks_TeamFull == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find CCSGameRules::TeamFull function signature");
+ }
+
+ if (!gH_DHooks_TeamFull.Enable(Hook_Pre, DHooks_OnTeamFull_Pre))
+ {
+ SetFailState("Failed to enable detour on CCSGameRules::TeamFull");
+ }
+
+ // Remove bot auto duck behavior.
+ gA_BotDuckAddr = gameData.GetAddress("BotDuck");
+ gI_BotDuckPatchLength = gameData.GetOffset("BotDuckPatchLength");
+ for (int i = 0; i < gI_BotDuckPatchLength; i++)
+ {
+ gI_BotDuckPatchRestore[i] = LoadFromAddress(gA_BotDuckAddr + view_as<Address>(i), NumberType_Int8);
+ StoreToAddress(gA_BotDuckAddr + view_as<Address>(i), 0x90, NumberType_Int8);
+ }
+ delete gameData;
+}
+
+static void UpdateCurrentMap()
+{
+ GetCurrentMapDisplayName(gC_CurrentMap, sizeof(gC_CurrentMap));
+ gI_CurrentMapFileSize = GetCurrentMapFileSize();
+}
+
+
+// =====[ PUBLIC ]=====
+
+// NOTE: These serialisation functions were made because the internal data layout of enum structs can change.
+void TickDataToArray(ReplayTickData tickData, any result[RP_V2_TICK_DATA_BLOCKSIZE])
+{
+ // NOTE: HAS to match ReplayTickData exactly!
+ result[0] = tickData.deltaFlags;
+ result[1] = tickData.deltaFlags2;
+ result[2] = tickData.vel[0];
+ result[3] = tickData.vel[1];
+ result[4] = tickData.vel[2];
+ result[5] = tickData.mouse[0];
+ result[6] = tickData.mouse[1];
+ result[7] = tickData.origin[0];
+ result[8] = tickData.origin[1];
+ result[9] = tickData.origin[2];
+ result[10] = tickData.angles[0];
+ result[11] = tickData.angles[1];
+ result[12] = tickData.angles[2];
+ result[13] = tickData.velocity[0];
+ result[14] = tickData.velocity[1];
+ result[15] = tickData.velocity[2];
+ result[16] = tickData.flags;
+ result[17] = tickData.packetsPerSecond;
+ result[18] = tickData.laggedMovementValue;
+ result[19] = tickData.buttonsForced;
+}
+
+void TickDataFromArray(any array[RP_V2_TICK_DATA_BLOCKSIZE], ReplayTickData result)
+{
+ // NOTE: HAS to match ReplayTickData exactly!
+ result.deltaFlags = array[0];
+ result.deltaFlags2 = array[1];
+ result.vel[0] = array[2];
+ result.vel[1] = array[3];
+ result.vel[2] = array[4];
+ result.mouse[0] = array[5];
+ result.mouse[1] = array[6];
+ result.origin[0] = array[7];
+ result.origin[1] = array[8];
+ result.origin[2] = array[9];
+ result.angles[0] = array[10];
+ result.angles[1] = array[11];
+ result.angles[2] = array[12];
+ result.velocity[0] = array[13];
+ result.velocity[1] = array[14];
+ result.velocity[2] = array[15];
+ result.flags = array[16];
+ result.packetsPerSecond = array[17];
+ result.laggedMovementValue = array[18];
+ result.buttonsForced = array[19];
+}
diff --git a/sourcemod/scripting/gokz-replays/api.sp b/sourcemod/scripting/gokz-replays/api.sp
new file mode 100644
index 0000000..3c115e1
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/api.sp
@@ -0,0 +1,78 @@
+static GlobalForward H_OnReplaySaved;
+static GlobalForward H_OnReplayDiscarded;
+static GlobalForward H_OnTimerEnd_Post;
+
+// =====[ NATIVES ]=====
+
+void CreateNatives()
+{
+ CreateNative("GOKZ_RP_GetPlaybackInfo", Native_RP_GetPlaybackInfo);
+ CreateNative("GOKZ_RP_LoadJumpReplay", Native_RP_LoadJumpReplay);
+ CreateNative("GOKZ_RP_UpdateReplayControlMenu", Native_RP_UpdateReplayControlMenu);
+}
+
+public int Native_RP_GetPlaybackInfo(Handle plugin, int numParams)
+{
+ HUDInfo info;
+ GetPlaybackState(GetNativeCell(1), info);
+ SetNativeArray(2, info, sizeof(HUDInfo));
+ return 1;
+}
+
+public int Native_RP_LoadJumpReplay(Handle plugin, int numParams)
+{
+ int len;
+ GetNativeStringLength(2, len);
+ char[] path = new char[len + 1];
+ GetNativeString(2, path, len + 1);
+ int botClient = LoadReplayBot(GetNativeCell(1), path);
+ return botClient;
+}
+
+public int Native_RP_UpdateReplayControlMenu(Handle plugin, int numParams)
+{
+ return view_as<int>(UpdateReplayControlMenu(GetNativeCell(1)));
+}
+
+// =====[ FORWARDS ]=====
+
+void CreateGlobalForwards()
+{
+ H_OnReplaySaved = new GlobalForward("GOKZ_RP_OnReplaySaved", ET_Event, Param_Cell, Param_Cell, Param_String, Param_Cell, Param_Cell, Param_Float, Param_String, Param_Cell);
+ H_OnReplayDiscarded = new GlobalForward("GOKZ_RP_OnReplayDiscarded", ET_Ignore, Param_Cell);
+ H_OnTimerEnd_Post = new GlobalForward("GOKZ_RP_OnTimerEnd_Post", ET_Ignore, Param_Cell, Param_String, Param_Cell, Param_Float, Param_Cell);
+}
+
+Action Call_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay)
+{
+ Action result;
+ Call_StartForward(H_OnReplaySaved);
+ Call_PushCell(client);
+ Call_PushCell(replayType);
+ Call_PushString(map);
+ Call_PushCell(course);
+ Call_PushCell(timeType);
+ Call_PushFloat(time);
+ Call_PushString(filePath);
+ Call_PushCell(tempReplay);
+ Call_Finish(result);
+ return result;
+}
+
+void Call_OnReplayDiscarded(int client)
+{
+ Call_StartForward(H_OnReplayDiscarded);
+ Call_PushCell(client);
+ Call_Finish();
+}
+
+void Call_OnTimerEnd_Post(int client, const char[] filePath, int course, float time, int teleportsUsed)
+{
+ Call_StartForward(H_OnTimerEnd_Post);
+ Call_PushCell(client);
+ Call_PushString(filePath);
+ Call_PushCell(course);
+ Call_PushFloat(time);
+ Call_PushCell(teleportsUsed);
+ Call_Finish();
+}
diff --git a/sourcemod/scripting/gokz-replays/commands.sp b/sourcemod/scripting/gokz-replays/commands.sp
new file mode 100644
index 0000000..43251f6
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/commands.sp
@@ -0,0 +1,55 @@
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_replay", CommandReplay, "[KZ] Open the replay loading menu.");
+ RegConsoleCmd("sm_replaycontrols", CommandReplayControls, "[KZ] Toggle the replay control menu.");
+ RegConsoleCmd("sm_rpcontrols", CommandReplayControls, "[KZ] Toggle the replay control menu.");
+ RegConsoleCmd("sm_replaygoto", CommandReplayGoto, "[KZ] Skip to a specific time in the replay (hh:mm:ss).");
+ RegConsoleCmd("sm_rpgoto", CommandReplayGoto, "[KZ] Skip to a specific time in the replay (hh:mm:ss).");
+}
+
+public Action CommandReplay(int client, int args)
+{
+ DisplayReplayModeMenu(client);
+ return Plugin_Handled;
+}
+
+public Action CommandReplayControls(int client, int args)
+{
+ ToggleReplayControls(client);
+ return Plugin_Handled;
+}
+
+public Action CommandReplayGoto(int client, int args)
+{
+ int seconds;
+ char timeString[32], split[3][32];
+
+ GetCmdArgString(timeString, sizeof(timeString));
+ int res = ExplodeString(timeString, ":", split, 3, 32, false);
+ switch (res)
+ {
+ case 1:
+ {
+ seconds = StringToInt(split[0]);
+ }
+
+ case 2:
+ {
+ seconds = StringToInt(split[0]) * 60 + StringToInt(split[1]);
+ }
+
+ case 3:
+ {
+ seconds = StringToInt(split[0]) * 3600 + StringToInt(split[1]) * 60 + StringToInt(split[2]);
+ }
+
+ default:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Replay Controls - Invalid Time");
+ return Plugin_Handled;
+ }
+ }
+
+ TrySkipToTime(client, seconds);
+ return Plugin_Handled;
+}
diff --git a/sourcemod/scripting/gokz-replays/controls.sp b/sourcemod/scripting/gokz-replays/controls.sp
new file mode 100644
index 0000000..cda7f07
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/controls.sp
@@ -0,0 +1,224 @@
+/*
+ Lets player control the replay bot.
+*/
+
+#define ITEM_INFO_PAUSE "pause"
+#define ITEM_INFO_SKIP "skip"
+#define ITEM_INFO_REWIND "rewind"
+#define ITEM_INFO_FREECAM "freecam"
+
+static int controllingPlayer[RP_MAX_BOTS];
+static int botTeleports[RP_MAX_BOTS];
+static bool showReplayControls[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+void OnPlayerRunCmdPost_ReplayControls(int client, int cmdnum)
+{
+ // Let the HUD plugin takes care of this if possible.
+ if (cmdnum % 6 == 3 && !gB_GOKZHUD)
+ {
+ UpdateReplayControlMenu(client);
+ }
+}
+
+bool UpdateReplayControlMenu(int client)
+{
+ if (!IsValidClient(client) || IsFakeClient(client))
+ {
+ return false;
+ }
+
+ int botClient = GetObserverTarget(client);
+ int bot = GetBotFromClient(botClient);
+ if (bot == -1)
+ {
+ return false;
+ }
+
+ if (!IsReplayBotControlled(bot, botClient) && !InBreather(bot))
+ {
+ CancelReplayControlsForBot(bot);
+ controllingPlayer[bot] = client;
+ }
+ else if (controllingPlayer[bot] != client)
+ {
+ return false;
+ }
+
+ if (showReplayControls[client] &&
+ GOKZ_HUD_GetOption(client, HUDOption_ShowControls) == ReplayControls_Enabled)
+ {
+ // We have to update this often if bot uses teleports.
+ if (GetClientMenu(client) == MenuSource_None ||
+ GOKZ_HUD_GetMenuShowing(client) && GetClientAvgLoss(client, NetFlow_Both) > EPSILON ||
+ GOKZ_HUD_GetMenuShowing(client) && GOKZ_HUD_GetOption(client, HUDOption_TimerText) == TimerText_TPMenu ||
+ GOKZ_HUD_GetMenuShowing(client) && PlaybackGetTeleports(bot) > 0)
+ {
+ botTeleports[bot] = PlaybackGetTeleports(bot);
+ ShowReplayControlMenu(client, bot);
+ }
+ return true;
+ }
+ return false;
+}
+
+void ShowReplayControlMenu(int client, int bot)
+{
+ char text[256];
+
+ Menu menu = new Menu(MenuHandler_ReplayControls);
+ menu.OptionFlags = MENUFLAG_NO_SOUND;
+ menu.Pagination = MENU_NO_PAGINATION;
+ menu.ExitButton = true;
+ if (gB_GOKZHUD)
+ {
+ if (GOKZ_HUD_GetOption(client, HUDOption_ShowSpectators) != ShowSpecs_Disabled &&
+ GOKZ_HUD_GetOption(client, HUDOption_SpecListPosition) == SpecListPosition_TPMenu)
+ {
+ HUDInfo info;
+ GetPlaybackState(client, info);
+ GOKZ_HUD_GetMenuSpectatorText(client, info, text, sizeof(text));
+ }
+ if (GOKZ_HUD_GetOption(client, HUDOption_TimerText) == TimerText_TPMenu)
+ {
+ Format(text, sizeof(text), "%s\n%T - %s", text, "Replay Controls - Title", client,
+ GOKZ_FormatTime(GetPlaybackTime(bot), GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Precise));
+ }
+ else
+ {
+ Format(text, sizeof(text), "%s%T", text, "Replay Controls - Title", client);
+ }
+ }
+ else
+ {
+ Format(text, sizeof(text), "%s%T", text, "Replay Controls - Title", client);
+ }
+
+
+ if (botTeleports[bot] > 0)
+ {
+ Format(text, sizeof(text), "%s\n%T", text, "Replay Controls - Teleports", client, botTeleports[bot]);
+ }
+
+ menu.SetTitle(text);
+
+ if (PlaybackPaused(bot))
+ {
+ FormatEx(text, sizeof(text), "%T", "Replay Controls - Resume", client);
+ menu.AddItem(ITEM_INFO_PAUSE, text);
+ }
+ else
+ {
+ FormatEx(text, sizeof(text), "%T", "Replay Controls - Pause", client);
+ menu.AddItem(ITEM_INFO_PAUSE, text);
+ }
+
+ FormatEx(text, sizeof(text), "%T", "Replay Controls - Skip", client);
+ menu.AddItem(ITEM_INFO_SKIP, text);
+
+ FormatEx(text, sizeof(text), "%T\n ", "Replay Controls - Rewind", client);
+ menu.AddItem(ITEM_INFO_REWIND, text);
+
+ FormatEx(text, sizeof(text), "%T", "Replay Controls - Freecam", client);
+ menu.AddItem(ITEM_INFO_FREECAM, text);
+
+ menu.Display(client, MENU_TIME_FOREVER);
+
+ if (gB_GOKZHUD)
+ {
+ GOKZ_HUD_SetMenuShowing(client, true);
+ }
+}
+
+void ToggleReplayControls(int client)
+{
+ if (showReplayControls[client])
+ {
+ CancelReplayControls(client);
+ }
+ else
+ {
+ showReplayControls[client] = true;
+ }
+}
+
+void EnableReplayControls(int client)
+{
+ showReplayControls[client] = true;
+}
+
+bool IsReplayBotControlled(int bot, int botClient)
+{
+ return IsValidClient(controllingPlayer[bot]) &&
+ (GetObserverTarget(controllingPlayer[bot]) == botClient ||
+ GetEntProp(controllingPlayer[bot], Prop_Send, "m_iObserverMode") == 6);
+}
+
+int MenuHandler_ReplayControls(Menu menu, MenuAction action, int param1, int param2)
+{
+ switch (action)
+ {
+ case MenuAction_Select:
+ {
+ if (!IsValidClient(param1))
+ {
+ return;
+ }
+
+ int bot = GetBotFromClient(GetObserverTarget(param1));
+ if (bot == -1 || controllingPlayer[bot] != param1)
+ {
+ return;
+ }
+
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+ if (StrEqual(info, ITEM_INFO_PAUSE, false))
+ {
+ PlaybackTogglePause(bot);
+ }
+ else if (StrEqual(info, ITEM_INFO_SKIP, false))
+ {
+ PlaybackSkipForward(bot);
+ }
+ else if (StrEqual(info, ITEM_INFO_REWIND, false))
+ {
+ PlaybackSkipBack(bot);
+ }
+ else if (StrEqual(info, ITEM_INFO_FREECAM, false))
+ {
+ SetEntProp(param1, Prop_Send, "m_iObserverMode", 6);
+ }
+ GOKZ_HUD_SetMenuShowing(param1, false);
+ }
+ case MenuAction_Cancel:
+ {
+ GOKZ_HUD_SetMenuShowing(param1, false);
+ if (param2 == MenuCancel_Exit)
+ {
+ CancelReplayControls(param1);
+ }
+ }
+ case MenuAction_End:
+ {
+ delete menu;
+ }
+ }
+}
+
+void CancelReplayControls(int client)
+{
+ if (IsValidClient(client) && showReplayControls[client])
+ {
+ CancelClientMenu(client);
+ showReplayControls[client] = false;
+ }
+}
+
+void CancelReplayControlsForBot(int bot)
+{
+ CancelReplayControls(controllingPlayer[bot]);
+}
diff --git a/sourcemod/scripting/gokz-replays/nav.sp b/sourcemod/scripting/gokz-replays/nav.sp
new file mode 100644
index 0000000..4e73c2f
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/nav.sp
@@ -0,0 +1,97 @@
+/*
+ Ensures that there is .nav file for the map so the server
+ does not to auto-generating one.
+*/
+
+
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_Nav()
+{
+ if (!CheckForNavFile())
+ {
+ GenerateNavFile();
+ }
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static bool CheckForNavFile()
+{
+ // Make sure there's a nav file
+ // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer
+
+ char mapPath[PLATFORM_MAX_PATH];
+ GetCurrentMap(mapPath, sizeof(mapPath));
+
+ char navFilePath[PLATFORM_MAX_PATH];
+ FormatEx(navFilePath, PLATFORM_MAX_PATH, "maps/%s.nav", mapPath);
+
+ return FileExists(navFilePath);
+}
+
+static void GenerateNavFile()
+{
+ // Generate (copy a) .nav file for the map
+ // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer
+
+ char mapPath[PLATFORM_MAX_PATH];
+ GetCurrentMap(mapPath, sizeof(mapPath));
+
+ char[] navFilePath = new char[PLATFORM_MAX_PATH];
+ FormatEx(navFilePath, PLATFORM_MAX_PATH, "maps/%s.nav", mapPath);
+
+ if (!FileExists(RP_NAV_FILE))
+ {
+ SetFailState("Failed to load file: \"%s\". Check that it exists.", RP_NAV_FILE);
+ }
+ File_Copy(RP_NAV_FILE, navFilePath);
+ ForceChangeLevel(gC_CurrentMap, "[gokz-replays] Generate .nav file.");
+}
+
+/*
+ * Copies file source to destination
+ * Based on code of javalia:
+ * http://forums.alliedmods.net/showthread.php?t=159895
+ *
+ * Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer
+ *
+ * @param source Input file
+ * @param destination Output file
+ */
+static bool File_Copy(const char[] source, const char[] destination)
+{
+ File file_source = OpenFile(source, "rb");
+
+ if (file_source == null)
+ {
+ return false;
+ }
+
+ File file_destination = OpenFile(destination, "wb");
+
+ if (file_destination == null)
+ {
+ delete file_source;
+
+ return false;
+ }
+
+ int[] buffer = new int[32];
+ int cache = 0;
+
+ while (!IsEndOfFile(file_source))
+ {
+ cache = ReadFile(file_source, buffer, 32, 1);
+
+ file_destination.Write(buffer, cache, 1);
+ }
+
+ delete file_source;
+ delete file_destination;
+
+ return true;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-replays/playback.sp b/sourcemod/scripting/gokz-replays/playback.sp
new file mode 100644
index 0000000..b3f6865
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/playback.sp
@@ -0,0 +1,1501 @@
+/*
+ Bot replay playback logic and processes.
+
+ The recorded files are read and their information and tick data
+ stored into variables. A bot is then used to playback the recorded
+ data by setting it's origin, velocity, etc. in OnPlayerRunCmd.
+*/
+
+
+
+static int preAndPostRunTickCount;
+
+static int playbackTick[RP_MAX_BOTS];
+static ArrayList playbackTickData[RP_MAX_BOTS];
+static bool inBreather[RP_MAX_BOTS];
+static float breatherStartTime[RP_MAX_BOTS];
+
+// Original bot caller, needed for OnClientPutInServer callback
+static int botCaller[RP_MAX_BOTS];
+// Original bot name after creation by bot_add, needed for bot removal
+static char botName[RP_MAX_BOTS][MAX_NAME_LENGTH];
+static bool botInGame[RP_MAX_BOTS];
+static int botClient[RP_MAX_BOTS];
+static bool botDataLoaded[RP_MAX_BOTS];
+static int botReplayType[RP_MAX_BOTS];
+static int botReplayVersion[RP_MAX_BOTS];
+static int botSteamAccountID[RP_MAX_BOTS];
+static int botCourse[RP_MAX_BOTS];
+static int botMode[RP_MAX_BOTS];
+static int botStyle[RP_MAX_BOTS];
+static float botTime[RP_MAX_BOTS];
+static int botTimeTicks[RP_MAX_BOTS];
+static char botAlias[RP_MAX_BOTS][MAX_NAME_LENGTH];
+static bool botPaused[RP_MAX_BOTS];
+static bool botPlaybackPaused[RP_MAX_BOTS];
+static int botKnife[RP_MAX_BOTS];
+static int botWeapon[RP_MAX_BOTS];
+static int botJumpType[RP_MAX_BOTS];
+static float botJumpDistance[RP_MAX_BOTS];
+static int botJumpBlockDistance[RP_MAX_BOTS];
+
+static int timeOnGround[RP_MAX_BOTS];
+static int timeInAir[RP_MAX_BOTS];
+static int botTeleportsUsed[RP_MAX_BOTS];
+static int botCurrentTeleport[RP_MAX_BOTS];
+static int botButtons[RP_MAX_BOTS];
+static MoveType botMoveType[RP_MAX_BOTS];
+static float botTakeoffSpeed[RP_MAX_BOTS];
+static float botSpeed[RP_MAX_BOTS];
+static float botLastOrigin[RP_MAX_BOTS][3];
+static bool hitBhop[RP_MAX_BOTS];
+static bool hitPerf[RP_MAX_BOTS];
+static bool botJumped[RP_MAX_BOTS];
+static bool botIsTakeoff[RP_MAX_BOTS];
+static bool botJustTeleported[RP_MAX_BOTS];
+static float botLandingSpeed[RP_MAX_BOTS];
+
+
+
+// =====[ PUBLIC ]=====
+
+// Returns the client index of the replay bot, or -1 otherwise
+int LoadReplayBot(int client, char[] path)
+{
+ // Safeguard Check
+ if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client))
+ {
+ if (!GOKZ_GetPaused(client) && !GOKZ_GetCanPause(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked");
+ GOKZ_PlayErrorSound(client);
+ return -1;
+ }
+ }
+ int bot;
+ if (GetBotsInUse() < RP_MAX_BOTS)
+ {
+ bot = GetUnusedBot();
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Bots Available");
+ GOKZ_PlayErrorSound(client);
+ return -1;
+ }
+
+ if (bot == -1)
+ {
+ LogError("Unused bot could not be found even though only %d out of %d are known to be in use.",
+ GetBotsInUse(), RP_MAX_BOTS);
+ GOKZ_PlayErrorSound(client);
+ return -1;
+ }
+
+ if (!LoadPlayback(client, bot, path))
+ {
+ GOKZ_PlayErrorSound(client);
+ return -1;
+ }
+
+ ServerCommand("bot_add");
+ botCaller[bot] = client;
+ return botClient[bot];
+}
+
+// Passes the current state of the replay into the HUDInfo struct
+void GetPlaybackState(int client, HUDInfo info)
+{
+ int bot, i;
+ for(i = 0; i < RP_MAX_BOTS; i++)
+ {
+ bot = botClient[i] == client ? i : bot;
+ }
+ if (i == RP_MAX_BOTS + 1) return;
+
+ if (playbackTickData[bot] == INVALID_HANDLE)
+ {
+ return;
+ }
+
+ info.TimerRunning = botReplayType[bot] == ReplayType_Jump ? false : true;
+ if (botReplayVersion[bot] == 1)
+ {
+ info.Time = playbackTick[bot] * GetTickInterval();
+ }
+ else if (botReplayVersion[bot] == 2)
+ {
+ if (playbackTick[bot] < preAndPostRunTickCount)
+ {
+ info.Time = 0.0;
+ }
+ else if (playbackTick[bot] >= playbackTickData[bot].Length - preAndPostRunTickCount)
+ {
+ info.Time = botTime[bot];
+ }
+ else if (playbackTick[bot] >= preAndPostRunTickCount)
+ {
+ info.Time = (playbackTick[bot] - preAndPostRunTickCount) * GetTickInterval();
+ }
+ }
+ info.TimerRunning = true;
+ info.TimeType = botTeleportsUsed[bot] > 0 ? TimeType_Nub : TimeType_Pro;
+ info.Speed = botSpeed[bot];
+ info.Paused = false;
+ info.OnLadder = (botMoveType[bot] == MOVETYPE_LADDER);
+ info.Noclipping = false;
+ info.OnGround = Movement_GetOnGround(client);
+ info.Ducking = botButtons[bot] & IN_DUCK > 0;
+ info.ID = botClient[bot];
+ info.Jumped = botJumped[bot];
+ info.HitBhop = hitBhop[bot];
+ info.HitPerf = hitPerf[bot];
+ info.Buttons = botButtons[bot];
+ info.TakeoffSpeed = botTakeoffSpeed[bot];
+ info.IsTakeoff = botIsTakeoff[bot] && !Movement_GetOnGround(client);
+ info.CurrentTeleport = botCurrentTeleport[bot];
+}
+
+int GetBotFromClient(int client)
+{
+ for (int bot = 0; bot < RP_MAX_BOTS; bot++)
+ {
+ if (botClient[bot] == client)
+ {
+ return bot;
+ }
+ }
+ return -1;
+}
+
+bool InBreather(int bot)
+{
+ return inBreather[bot];
+}
+
+bool PlaybackPaused(int bot)
+{
+ return botPlaybackPaused[bot];
+}
+
+void PlaybackTogglePause(int bot)
+{
+ if(botPlaybackPaused[bot])
+ {
+ botPlaybackPaused[bot] = false;
+ }
+ else
+ {
+ botPlaybackPaused[bot] = true;
+ }
+}
+
+void PlaybackSkipForward(int bot)
+{
+ if (playbackTick[bot] + RoundToZero(RP_SKIP_TIME / GetTickInterval()) < playbackTickData[bot].Length)
+ {
+ PlaybackSkipToTick(bot, playbackTick[bot] + RoundToZero(RP_SKIP_TIME / GetTickInterval()));
+ }
+}
+
+void PlaybackSkipBack(int bot)
+{
+ if (playbackTick[bot] < RoundToZero(RP_SKIP_TIME / GetTickInterval()))
+ {
+ PlaybackSkipToTick(bot, 0);
+ }
+ else
+ {
+ PlaybackSkipToTick(bot, playbackTick[bot] - RoundToZero(RP_SKIP_TIME / GetTickInterval()));
+ }
+}
+
+int PlaybackGetTeleports(int bot)
+{
+ return botCurrentTeleport[bot];
+}
+
+void TrySkipToTime(int client, int seconds)
+{
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ int tick = seconds * 128 + preAndPostRunTickCount;
+ int bot = GetBotFromClient(GetObserverTarget(client));
+
+ if (tick >= 0 && tick < playbackTickData[bot].Length)
+ {
+ PlaybackSkipToTick(bot, tick);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Replay Controls - Invalid Time");
+ }
+}
+
+float GetPlaybackTime(int bot)
+{
+ if (playbackTick[bot] < preAndPostRunTickCount)
+ {
+ return 0.0;
+ }
+ if (playbackTick[bot] >= playbackTickData[bot].Length - preAndPostRunTickCount)
+ {
+ return botTime[bot];
+ }
+ if (playbackTick[bot] >= preAndPostRunTickCount)
+ {
+ return (playbackTick[bot] - preAndPostRunTickCount) * GetTickInterval();
+ }
+
+ return 0.0;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Playback(int client)
+{
+ if (!IsFakeClient(client) || IsClientSourceTV(client))
+ {
+ return;
+ }
+
+ // Check if an unassigned bot has joined, and assign it
+ for (int bot; bot < RP_MAX_BOTS; bot++)
+ {
+ // Also check if the bot was created by us.
+ if (!botInGame[bot] && botCaller[bot] != 0)
+ {
+ botInGame[bot] = true;
+ botClient[bot] = client;
+ GetClientName(client, botName[bot], sizeof(botName[]));
+ // The bot won't receive its weapons properly if we don't wait a frame
+ RequestFrame(SetBotStuff, bot);
+ if (IsValidClient(botCaller[bot]))
+ {
+ MakePlayerSpectate(botCaller[bot], botClient[bot]);
+ botCaller[bot] = 0;
+ }
+ break;
+ }
+ }
+}
+
+void OnClientDisconnect_Playback(int client)
+{
+ for (int bot; bot < RP_MAX_BOTS; bot++)
+ {
+ if (botClient[bot] != client)
+ {
+ continue;
+ }
+
+ botInGame[bot] = false;
+ if (playbackTickData[bot] != null)
+ {
+ playbackTickData[bot].Clear(); // Clear it all out
+ botDataLoaded[bot] = false;
+ }
+ }
+}
+
+void OnPlayerRunCmd_Playback(int client, int &buttons, float vel[3], float angles[3])
+{
+ if (!IsFakeClient(client))
+ {
+ return;
+ }
+
+ for (int bot; bot < RP_MAX_BOTS; bot++)
+ {
+ // Check if not the bot we're looking for
+ if (!botInGame[bot] || botClient[bot] != client || !botDataLoaded[bot])
+ {
+ continue;
+ }
+
+ switch (botReplayVersion[bot])
+ {
+ case 1: PlaybackVersion1(client, bot, buttons);
+ case 2: PlaybackVersion2(client, bot, buttons, vel, angles);
+ }
+ break;
+ }
+}
+
+void OnPlayerRunCmdPost_Playback(int client)
+{
+ for (int bot; bot < RP_MAX_BOTS; bot++)
+ {
+ // Check if not the bot we're looking for
+ if (!botInGame[bot] || botClient[bot] != client || !botDataLoaded[bot])
+ {
+ continue;
+ }
+ if (botReplayVersion[bot] == 2)
+ {
+ PlaybackVersion2Post(client, bot);
+ }
+ break;
+ }
+}
+
+void GOKZ_OnOptionsLoaded_Playback(int client)
+{
+ for (int bot = 0; bot < RP_MAX_BOTS; bot++)
+ {
+ if (botClient[bot] == client)
+ {
+ // Reset its movement options as it might be wrongfully changed
+ GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]);
+ GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]);
+ }
+ }
+}
+// =====[ PRIVATE ]=====
+
+// Returns false if there was a problem loading the playback e.g. doesn't exist
+static bool LoadPlayback(int client, int bot, char[] path)
+{
+ if (!FileExists(path))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Replay Found");
+ return false;
+ }
+
+ File file = OpenFile(path, "rb");
+
+ // Check magic number in header
+ int magicNumber;
+ file.ReadInt32(magicNumber);
+ if (magicNumber != RP_MAGIC_NUMBER)
+ {
+ LogError("Failed to load invalid replay file: \"%s\".", path);
+ delete file;
+ return false;
+ }
+
+ // Check replay format version
+ int formatVersion;
+ file.ReadInt8(formatVersion);
+ switch(formatVersion)
+ {
+ case 1:
+ {
+ botReplayVersion[bot] = 1;
+ if (!LoadFormatVersion1Replay(file, bot))
+ {
+ return false;
+ }
+ }
+ case 2:
+ {
+ botReplayVersion[bot] = 2;
+ if (!LoadFormatVersion2Replay(file, client, bot))
+ {
+ return false;
+ }
+ }
+
+ default:
+ {
+ LogError("Failed to load replay file with unsupported format version: \"%s\".", path);
+ delete file;
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool LoadFormatVersion1Replay(File file, int bot)
+{
+ // Old replays only support runs, not jumps
+ botReplayType[bot] = ReplayType_Run;
+
+ int length;
+
+ // GOKZ version
+ file.ReadInt8(length);
+ char[] gokzVersion = new char[length + 1];
+ file.ReadString(gokzVersion, length, length);
+ gokzVersion[length] = '\0';
+
+ // Map name
+ file.ReadInt8(length);
+ char[] mapName = new char[length + 1];
+ file.ReadString(mapName, length, length);
+ mapName[length] = '\0';
+
+ // Some integers...
+ file.ReadInt32(botCourse[bot]);
+ file.ReadInt32(botMode[bot]);
+ file.ReadInt32(botStyle[bot]);
+
+ // Old replays don't store the weapon information
+ botKnife[bot] = CS_WeaponIDToItemDefIndex(CSWeapon_KNIFE);
+ botWeapon[bot] = (botMode[bot] == Mode_Vanilla) ? -1 : CS_WeaponIDToItemDefIndex(CSWeapon_USP_SILENCER);
+
+ // Time
+ int timeAsInt;
+ file.ReadInt32(timeAsInt);
+ botTime[bot] = view_as<float>(timeAsInt);
+
+ // Some integers...
+ file.ReadInt32(botTeleportsUsed[bot]);
+ file.ReadInt32(botSteamAccountID[bot]);
+
+ // SteamID2
+ file.ReadInt8(length);
+ char[] steamID2 = new char[length + 1];
+ file.ReadString(steamID2, length, length);
+ steamID2[length] = '\0';
+
+ // IP
+ file.ReadInt8(length);
+ char[] IP = new char[length + 1];
+ file.ReadString(IP, length, length);
+ IP[length] = '\0';
+
+ // Alias
+ file.ReadInt8(length);
+ file.ReadString(botAlias[bot], sizeof(botAlias[]), length);
+ botAlias[bot][length] = '\0';
+
+ // Read tick data
+ file.ReadInt32(length);
+
+ // Setup playback tick data array list
+ if (playbackTickData[bot] == null)
+ {
+ playbackTickData[bot] = new ArrayList(IntMax(RP_V1_TICK_DATA_BLOCKSIZE, sizeof(ReplayTickData)), length);
+ }
+ else
+ { // Make sure it's all clear and the correct size
+ playbackTickData[bot].Clear();
+ playbackTickData[bot].Resize(length);
+ }
+
+ // The replay has no replay data, this shouldn't happen normally,
+ // but this would cause issues in other code, so we don't even try to load this.
+ if (length == 0)
+ {
+ delete file;
+ return false;
+ }
+
+ any tickData[RP_V1_TICK_DATA_BLOCKSIZE];
+ for (int i = 0; i < length; i++)
+ {
+ file.Read(tickData, RP_V1_TICK_DATA_BLOCKSIZE, 4);
+ playbackTickData[bot].Set(i, view_as<float>(tickData[0]), 0); // origin[0]
+ playbackTickData[bot].Set(i, view_as<float>(tickData[1]), 1); // origin[1]
+ playbackTickData[bot].Set(i, view_as<float>(tickData[2]), 2); // origin[2]
+ playbackTickData[bot].Set(i, view_as<float>(tickData[3]), 3); // angles[0]
+ playbackTickData[bot].Set(i, view_as<float>(tickData[4]), 4); // angles[1]
+ playbackTickData[bot].Set(i, view_as<int>(tickData[5]), 5); // buttons
+ playbackTickData[bot].Set(i, view_as<int>(tickData[6]), 6); // flags
+ }
+
+ playbackTick[bot] = 0;
+ botDataLoaded[bot] = true;
+
+ delete file;
+ return true;
+}
+
+static bool LoadFormatVersion2Replay(File file, int client, int bot)
+{
+ int length;
+
+ // Replay type
+ int replayType;
+ file.ReadInt8(replayType);
+
+ // GOKZ version
+ file.ReadInt8(length);
+ char[] gokzVersion = new char[length + 1];
+ file.ReadString(gokzVersion, length, length);
+ gokzVersion[length] = '\0';
+
+ // Map name
+ file.ReadInt8(length);
+ char[] mapName = new char[length + 1];
+ file.ReadString(mapName, length, length);
+ mapName[length] = '\0';
+ if (!StrEqual(mapName, gC_CurrentMap))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Replay Menu - Wrong Map", mapName);
+ delete file;
+ return false;
+ }
+
+ // Map filesize
+ int mapFileSize;
+ file.ReadInt32(mapFileSize);
+
+ // Server IP
+ int serverIP;
+ file.ReadInt32(serverIP);
+
+ // Timestamp
+ int timestamp;
+ file.ReadInt32(timestamp);
+
+ // Player Alias
+ file.ReadInt8(length);
+ file.ReadString(botAlias[bot], sizeof(botAlias[]), length);
+ botAlias[bot][length] = '\0';
+
+ // Player Steam ID
+ int steamID;
+ file.ReadInt32(steamID);
+
+ // Mode
+ file.ReadInt8(botMode[bot]);
+
+ // Style
+ file.ReadInt8(botStyle[bot]);
+
+ // Player Sensitivity
+ int intPlayerSensitivity;
+ file.ReadInt32(intPlayerSensitivity);
+ float playerSensitivity = view_as<float>(intPlayerSensitivity);
+
+ // Player MYAW
+ int intPlayerMYaw;
+ file.ReadInt32(intPlayerMYaw);
+ float playerMYaw = view_as<float>(intPlayerMYaw);
+
+ // Tickrate
+ int tickrateAsInt;
+ file.ReadInt32(tickrateAsInt);
+ float tickrate = view_as<float>(tickrateAsInt);
+ if (tickrate != RoundToZero(1 / GetTickInterval()))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Replay Menu - Wrong Tickrate", tickrate, (RoundToZero(1 / GetTickInterval())));
+ delete file;
+ return false;
+ }
+
+ // Tick Count
+ int tickCount;
+ file.ReadInt32(tickCount);
+
+ // The replay has no replay data, this shouldn't happen normally,
+ // but this would cause issues in other code, so we don't even try to load this.
+ if (tickCount == 0)
+ {
+ delete file;
+ return false;
+ }
+
+ // Equipped Weapon
+ file.ReadInt32(botWeapon[bot]);
+
+ // Equipped Knife
+ file.ReadInt32(botKnife[bot]);
+
+ // Big spit to console
+ PrintToConsole(client, "Replay Type: %d\nGOKZ Version: %s\nMap Name: %s\nMap Filesize: %d\nServer IP: %d\nTimestamp: %d\nPlayer Alias: %s\nPlayer Steam ID: %d\nMode: %d\nStyle: %d\nPlayer Sensitivity: %f\nPlayer m_yaw: %f\nTickrate: %f\nTick Count: %d\nWeapon: %d\nKnife: %d", replayType, gokzVersion, mapName, mapFileSize, serverIP, timestamp, botAlias[bot], steamID, botMode[bot], botStyle[bot], playerSensitivity, playerMYaw, tickrate, tickCount, botWeapon[bot], botKnife[bot]);
+
+ switch(replayType)
+ {
+ case ReplayType_Run:
+ {
+ // Time
+ int timeAsInt;
+ file.ReadInt32(timeAsInt);
+ botTime[bot] = view_as<float>(timeAsInt);
+ botTimeTicks[bot] = RoundToNearest(botTime[bot] * tickrate);
+
+ // Course
+ file.ReadInt8(botCourse[bot]);
+
+ // Teleports Used
+ file.ReadInt32(botTeleportsUsed[bot]);
+
+ // Type
+ botReplayType[bot] = ReplayType_Run;
+
+ // Finish spit to console
+ PrintToConsole(client, "Time: %f\nCourse: %d\nTeleports Used: %d", botTime[bot], botCourse[bot], botTeleportsUsed[bot]);
+ }
+ case ReplayType_Cheater:
+ {
+ // Reason
+ int reason;
+ file.ReadInt8(reason);
+
+ // Type
+ botReplayType[bot] = ReplayType_Cheater;
+
+ // Finish spit to console
+ PrintToConsole(client, "AC Reason: %s", gC_ACReasons[reason]);
+ }
+ case ReplayType_Jump:
+ {
+ // Jump Type
+ file.ReadInt8(botJumpType[bot]);
+
+ // Distance
+ file.ReadInt32(view_as<int>(botJumpDistance[bot]));
+
+ // Block Distance
+ file.ReadInt32(botJumpBlockDistance[bot]);
+
+ // Strafe Count
+ int strafeCount;
+ file.ReadInt8(strafeCount);
+
+ // Sync
+ float sync;
+ file.ReadInt32(view_as<int>(sync));
+
+ // Pre
+ float pre;
+ file.ReadInt32(view_as<int>(pre));
+
+ // Max
+ float max;
+ file.ReadInt32(view_as<int>(max));
+
+ // Airtime
+ int airtime;
+ file.ReadInt32(airtime);
+
+ // Type
+ botReplayType[bot] = ReplayType_Jump;
+
+ // Finish spit to console
+ PrintToConsole(client, "Jump Type: %s\nJump Distance: %f\nBlock Distance: %d\nStrafe Count: %d\nSync: %f\n Pre: %f\nMax: %f\nAirtime: %d",
+ gC_JumpTypes[botJumpType[bot]], botJumpDistance[bot], botJumpBlockDistance[bot], strafeCount, sync, pre, max, airtime);
+ }
+ }
+
+ // Tick Data
+ // Setup playback tick data array list
+ if (playbackTickData[bot] == null)
+ {
+ playbackTickData[bot] = new ArrayList(IntMax(RP_V1_TICK_DATA_BLOCKSIZE, sizeof(ReplayTickData)));
+ }
+ else
+ {
+ playbackTickData[bot].Clear();
+ }
+
+ // Read tick data
+ preAndPostRunTickCount = RoundToZero(RP_PLAYBACK_BREATHER_TIME / GetTickInterval());
+ any tickDataArray[RP_V2_TICK_DATA_BLOCKSIZE];
+ for (int i = 0; i < tickCount; i++)
+ {
+ file.ReadInt32(tickDataArray[RPDELTA_DELTAFLAGS]);
+
+ for (int index = 1; index < sizeof(tickDataArray); index++)
+ {
+ int currentFlag = (1 << index);
+ if (tickDataArray[RPDELTA_DELTAFLAGS] & currentFlag)
+ {
+ file.ReadInt32(tickDataArray[index]);
+ }
+ }
+
+ ReplayTickData tickData;
+ TickDataFromArray(tickDataArray, tickData);
+ // HACK: Jump replays don't record proper length sometimes. I don't know why.
+ // This leads to oversized replays full of 0s at the end.
+ // So, we do this horrible check to dodge that issue.
+ if (tickData.origin[0] == 0 && tickData.origin[1] == 0 && tickData.origin[2] == 0 && tickData.angles[0] == 0 && tickData.angles[1] == 0)
+ {
+ break;
+ }
+ playbackTickData[bot].PushArray(tickData);
+ }
+
+ playbackTick[bot] = 0;
+ botDataLoaded[bot] = true;
+
+ delete file;
+
+ return true;
+}
+
+static void PlaybackVersion1(int client, int bot, int &buttons)
+{
+ int size = playbackTickData[bot].Length;
+ float repOrigin[3], repAngles[3];
+ int repButtons, repFlags;
+
+ // If first or last frame of the playback
+ if (playbackTick[bot] == 0 || playbackTick[bot] == (size - 1))
+ {
+ // Move the bot and pause them at that tick
+ repOrigin[0] = playbackTickData[bot].Get(playbackTick[bot], 0);
+ repOrigin[1] = playbackTickData[bot].Get(playbackTick[bot], 1);
+ repOrigin[2] = playbackTickData[bot].Get(playbackTick[bot], 2);
+ repAngles[0] = playbackTickData[bot].Get(playbackTick[bot], 3);
+ repAngles[1] = playbackTickData[bot].Get(playbackTick[bot], 4);
+ TeleportEntity(client, repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } ));
+
+ if (!inBreather[bot])
+ {
+ // Start the breather period
+ inBreather[bot] = true;
+ breatherStartTime[bot] = GetEngineTime();
+ if (playbackTick[bot] == (size - 1))
+ {
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+ }
+ }
+ else if (GetEngineTime() > breatherStartTime[bot] + RP_PLAYBACK_BREATHER_TIME)
+ {
+ // End the breather period
+ inBreather[bot] = false;
+ botPlaybackPaused[bot] = false;
+ if (playbackTick[bot] == 0)
+ {
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ }
+ // Start the bot if first tick. Clear bot if last tick.
+ playbackTick[bot]++;
+ if (playbackTick[bot] == size)
+ {
+ playbackTickData[bot].Clear(); // Clear it all out
+ botDataLoaded[bot] = false;
+ CancelReplayControlsForBot(bot);
+ ServerCommand("bot_kick %s", botName[bot]);
+ }
+ }
+ }
+ else
+ {
+ // Check whether somebody is actually spectating the bot
+ int spec;
+ for (spec = 1; spec < MAXPLAYERS + 1; spec++)
+ {
+ if (IsValidClient(spec) && GetObserverTarget(spec) == botClient[bot])
+ {
+ break;
+ }
+ }
+ if (spec == MAXPLAYERS + 1 && !IsReplayBotControlled(bot, botClient[bot]))
+ {
+ playbackTickData[bot].Clear();
+ botDataLoaded[bot] = false;
+ CancelReplayControlsForBot(bot);
+ ServerCommand("bot_kick %s", botName[bot]);
+ return;
+ }
+
+ // Load in the next tick
+ repOrigin[0] = playbackTickData[bot].Get(playbackTick[bot], 0);
+ repOrigin[1] = playbackTickData[bot].Get(playbackTick[bot], 1);
+ repOrigin[2] = playbackTickData[bot].Get(playbackTick[bot], 2);
+ repAngles[0] = playbackTickData[bot].Get(playbackTick[bot], 3);
+ repAngles[1] = playbackTickData[bot].Get(playbackTick[bot], 4);
+ repButtons = playbackTickData[bot].Get(playbackTick[bot], 5);
+ repFlags = playbackTickData[bot].Get(playbackTick[bot], 6);
+
+ // Check if the replay is paused
+ if (botPlaybackPaused[bot])
+ {
+ TeleportEntity(client, repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ return;
+ }
+
+ // Set velocity to travel from current origin to recorded origin
+ float currentOrigin[3], velocity[3];
+ Movement_GetOrigin(client, currentOrigin);
+ MakeVectorFromPoints(currentOrigin, repOrigin, velocity);
+ ScaleVector(velocity, 128.0); // Hard-coded 128 tickrate
+ TeleportEntity(client, NULL_VECTOR, repAngles, velocity);
+
+ // We need the velocity directly from the replay to calculate the speeds
+ // for the HUD.
+ MakeVectorFromPoints(botLastOrigin[bot], repOrigin, velocity);
+ ScaleVector(velocity, 128.0); // Hard-coded 128 tickrate
+ CopyVector(repOrigin, botLastOrigin[bot]);
+
+ botSpeed[bot] = GetVectorHorizontalLength(velocity);
+ buttons = repButtons;
+ botButtons[bot] = repButtons;
+
+ // Should the bot be ducking?!
+ if (repButtons & IN_DUCK || repFlags & FL_DUCKING)
+ {
+ buttons |= IN_DUCK;
+ }
+
+ // If the replay file says the bot's on the ground, then fine! Unless you're going too fast...
+ // Note that we don't mind if replay file says bot isn't on ground but the bot is.
+ if (repFlags & FL_ONGROUND && Movement_GetSpeed(client) < SPEED_NORMAL * 2)
+ {
+ if (timeInAir[bot] > 0)
+ {
+ botLandingSpeed[bot] = botSpeed[bot];
+ timeInAir[bot] = 0;
+ botIsTakeoff[bot] = false;
+ botJumped[bot] = false;
+ hitBhop[bot] = false;
+ hitPerf[bot] = false;
+ if (!Movement_GetOnGround(client))
+ {
+ timeOnGround[bot] = 0;
+ }
+ }
+
+ SetEntityFlags(client, GetEntityFlags(client) | FL_ONGROUND);
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+
+ timeOnGround[bot]++;
+ botTakeoffSpeed[bot] = botSpeed[bot];
+ }
+ else
+ {
+ if (timeInAir[bot] == 0)
+ {
+ botIsTakeoff[bot] = true;
+ botJumped[bot] = botButtons[bot] & IN_JUMP > 0;
+ hitBhop[bot] = (timeOnGround[bot] <= RP_MAX_BHOP_GROUND_TICKS) && botJumped[bot];
+
+ if (botMode[bot] == Mode_SimpleKZ)
+ {
+ hitPerf[bot] = timeOnGround[bot] < 3 && botJumped[bot];
+ }
+ else
+ {
+ hitPerf[bot] = timeOnGround[bot] < 2 && botJumped[bot];
+ }
+
+ if (hitPerf[bot])
+ {
+ if (botMode[bot] == Mode_SimpleKZ)
+ {
+ botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], (0.2 * botLandingSpeed[bot] + 200));
+ }
+ else if (botMode[bot] == Mode_KZTimer)
+ {
+ botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], 380.0);
+ }
+ else
+ {
+ botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], 286.0);
+ }
+ }
+ }
+ else
+ {
+ botJumped[bot] = false;
+ botIsTakeoff[bot] = false;
+ }
+
+ timeInAir[bot]++;
+ Movement_SetMovetype(client, MOVETYPE_NOCLIP);
+ }
+
+ playbackTick[bot]++;
+ }
+}
+void PlaybackVersion2(int client, int bot, int &buttons, float vel[3], float angles[3])
+{
+ int size = playbackTickData[bot].Length;
+ ReplayTickData prevTickData;
+ ReplayTickData currentTickData;
+
+ // If first or last frame of the playback
+ if (playbackTick[bot] == 0 || playbackTick[bot] == (size - 1))
+ {
+ // Move the bot and pause them at that tick
+ playbackTickData[bot].GetArray(playbackTick[bot], currentTickData);
+ playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData);
+ TeleportEntity(client, currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } ));
+
+ if (!inBreather[bot])
+ {
+ // Start the breather period
+ inBreather[bot] = true;
+ breatherStartTime[bot] = GetEngineTime();
+ }
+ else if (GetEngineTime() > breatherStartTime[bot] + RP_PLAYBACK_BREATHER_TIME)
+ {
+ // End the breather period
+ inBreather[bot] = false;
+ botPlaybackPaused[bot] = false;
+
+ // Start the bot if first tick. Clear bot if last tick.
+ playbackTick[bot]++;
+ if (playbackTick[bot] == size)
+ {
+ playbackTickData[bot].Clear(); // Clear it all out
+ botDataLoaded[bot] = false;
+ CancelReplayControlsForBot(bot);
+ ServerCommand("bot_kick %s", botName[bot]);
+ }
+ }
+ }
+ else
+ {
+ // Check whether somebody is actually spectating the bot
+ int spec;
+ for (spec = 1; spec < MAXPLAYERS + 1; spec++)
+ {
+ if (IsValidClient(spec) && GetObserverTarget(spec) == botClient[bot])
+ {
+ break;
+ }
+ }
+ if (spec == MAXPLAYERS + 1 && !IsReplayBotControlled(bot, botClient[bot]))
+ {
+ playbackTickData[bot].Clear();
+ botDataLoaded[bot] = false;
+ CancelReplayControlsForBot(bot);
+ ServerCommand("bot_kick %s", botName[bot]);
+ return;
+ }
+
+ // Load in the next tick
+ playbackTickData[bot].GetArray(playbackTick[bot], currentTickData);
+ playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData);
+
+ // Check if the replay is paused
+ if (botPlaybackPaused[bot])
+ {
+ TeleportEntity(client, currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ return;
+ }
+
+ // Play timer start/end sound, if necessary. Reset teleports
+ if (playbackTick[bot] == preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run)
+ {
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ botCurrentTeleport[bot] = 0;
+ }
+ if (playbackTick[bot] == botTimeTicks[bot] + preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run)
+ {
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+ }
+ // We use the previous position/velocity data to recreate sounds accurately.
+ // This might not be necessary as we already did do this in OnPlayerRunCmdPost of last tick,
+ // but we do it again just in case the values don't match up somehow (eg. collision with moving objects?)
+ TeleportEntity(client, NULL_VECTOR, prevTickData.angles, prevTickData.velocity);
+ // TeleportEntity does not set the absolute origin and velocity so we need to do it
+ // to prevent inaccurate eye position interpolation.
+ SetEntPropVector(client, Prop_Data, "m_vecVelocity", prevTickData.velocity);
+ SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", prevTickData.velocity);
+
+ SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", prevTickData.origin);
+ SetEntPropVector(client, Prop_Data, "m_vecOrigin", prevTickData.origin);
+
+
+ // Set buttons and potential inputs.
+ int newButtons;
+ if (currentTickData.flags & RP_IN_ATTACK)
+ {
+ newButtons |= IN_ATTACK;
+ }
+ if (currentTickData.flags & RP_IN_ATTACK2)
+ {
+ newButtons |= IN_ATTACK2;
+ }
+ if (currentTickData.flags & RP_IN_JUMP)
+ {
+ newButtons |= IN_JUMP;
+ }
+ if (currentTickData.flags & RP_IN_DUCK || currentTickData.flags & RP_FL_DUCKING)
+ {
+ newButtons |= IN_DUCK;
+ }
+ // Few assumptions here because the replay doesn't track them: Player doesn't use +klook or +strafe.
+ // If the assumptions are wrong we will just end up with wrong sound prediction, no big deal.
+ if (currentTickData.flags & RP_IN_FORWARD)
+ {
+ newButtons |= IN_FORWARD;
+ vel[0] += RP_PLAYER_ACCELSPEED;
+ }
+ if (currentTickData.flags & RP_IN_BACK)
+ {
+ newButtons |= IN_BACK;
+ vel[0] -= RP_PLAYER_ACCELSPEED;
+ }
+ if (currentTickData.flags & RP_IN_MOVELEFT)
+ {
+ newButtons |= IN_MOVELEFT;
+ vel[1] -= RP_PLAYER_ACCELSPEED;
+ }
+ if (currentTickData.flags & RP_IN_MOVERIGHT)
+ {
+ newButtons |= IN_MOVERIGHT;
+ vel[1] += RP_PLAYER_ACCELSPEED;
+ }
+ if (currentTickData.flags & RP_IN_LEFT)
+ {
+ newButtons |= IN_LEFT;
+ }
+ if (currentTickData.flags & RP_IN_RIGHT)
+ {
+ newButtons |= IN_RIGHT;
+ }
+ if (currentTickData.flags & RP_IN_RELOAD)
+ {
+ newButtons |= IN_RELOAD;
+ }
+ if (currentTickData.flags & RP_IN_SPEED)
+ {
+ newButtons |= IN_SPEED;
+ }
+ buttons = newButtons;
+ botButtons[bot] = buttons;
+ // The angles might be wrong if the player teleports, but this should only affect sound prediction.
+ angles = currentTickData.angles;
+
+ // Set the bot's MoveType
+ MoveType replayMoveType = view_as<MoveType>(prevTickData.flags & RP_MOVETYPE_MASK);
+ botMoveType[bot] = replayMoveType;
+ if (replayMoveType == MOVETYPE_WALK)
+ {
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ }
+ else if (replayMoveType == MOVETYPE_LADDER)
+ {
+ botPaused[bot] = false;
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+ else
+ {
+ Movement_SetMovetype(client, MOVETYPE_NOCLIP);
+ }
+ // Set some variables
+ if (currentTickData.flags & RP_TELEPORT_TICK)
+ {
+ botJustTeleported[bot] = true;
+ botCurrentTeleport[bot]++;
+ }
+
+ if (currentTickData.flags & RP_TAKEOFF_TICK)
+ {
+ hitPerf[bot] = currentTickData.flags & RP_HIT_PERF > 0;
+ botIsTakeoff[bot] = true;
+ botTakeoffSpeed[bot] = GetVectorHorizontalLength(currentTickData.velocity);
+ }
+
+ if ((currentTickData.flags & RP_SECONDARY_EQUIPPED) && !IsCurrentWeaponSecondary(client))
+ {
+ int item = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY);
+ if (item != -1)
+ {
+ char name[64];
+ GetEntityClassname(item, name, sizeof(name));
+ FakeClientCommand(client, "use %s", name);
+ }
+ }
+ else if (!(currentTickData.flags & RP_SECONDARY_EQUIPPED) && IsCurrentWeaponSecondary(client))
+ {
+ int item = GetPlayerWeaponSlot(client, CS_SLOT_KNIFE);
+ if (item != -1)
+ {
+ char name[64];
+ GetEntityClassname(item, name, sizeof(name));
+ FakeClientCommand(client, "use %s", name);
+ }
+ }
+
+ #if defined DEBUG
+ if(!botPlaybackPaused[bot])
+ {
+ PrintToServer("Tick: %d", playbackTick[bot]);
+ PrintToServer("X %f \nY %f \nZ %f\nPitch %f\nYaw %f", currentTickData.origin[0], currentTickData.origin[1], currentTickData.origin[2], currentTickData.angles[0], currentTickData.angles[1]);
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK");
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER");
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP");
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NONE");
+
+ if(currentTickData.flags & RP_IN_ATTACK) PrintToServer("IN_ATTACK");
+ if(currentTickData.flags & RP_IN_ATTACK2) PrintToServer("IN_ATTACK2");
+ if(currentTickData.flags & RP_IN_JUMP) PrintToServer("IN_JUMP");
+ if(currentTickData.flags & RP_IN_DUCK) PrintToServer("IN_DUCK");
+ if(currentTickData.flags & RP_IN_FORWARD) PrintToServer("IN_FORWARD");
+ if(currentTickData.flags & RP_IN_BACK) PrintToServer("IN_BACK");
+ if(currentTickData.flags & RP_IN_LEFT) PrintToServer("IN_LEFT");
+ if(currentTickData.flags & RP_IN_RIGHT) PrintToServer("IN_RIGHT");
+ if(currentTickData.flags & RP_IN_MOVELEFT) PrintToServer("IN_MOVELEFT");
+ if(currentTickData.flags & RP_IN_MOVERIGHT) PrintToServer("IN_MOVERIGHT");
+ if(currentTickData.flags & RP_IN_RELOAD) PrintToServer("IN_RELOAD");
+ if(currentTickData.flags & RP_IN_SPEED) PrintToServer("IN_SPEED");
+ if(currentTickData.flags & RP_IN_USE) PrintToServer("IN_USE");
+ if(currentTickData.flags & RP_IN_BULLRUSH) PrintToServer("IN_BULLRUSH");
+
+ if(currentTickData.flags & RP_FL_ONGROUND) PrintToServer("FL_ONGROUND");
+ if(currentTickData.flags & RP_FL_DUCKING ) PrintToServer("FL_DUCKING");
+ if(currentTickData.flags & RP_FL_SWIM) PrintToServer("FL_SWIM");
+ if(currentTickData.flags & RP_UNDER_WATER) PrintToServer("WATERLEVEL!=0");
+ if(currentTickData.flags & RP_TELEPORT_TICK) PrintToServer("TELEPORT");
+ if(currentTickData.flags & RP_TAKEOFF_TICK) PrintToServer("TAKEOFF");
+ if(currentTickData.flags & RP_HIT_PERF) PrintToServer("PERF");
+ if(currentTickData.flags & RP_SECONDARY_EQUIPPED) PrintToServer("SECONDARY_WEAPON_EQUIPPED");
+ PrintToServer("==============================================================");
+ }
+ #endif
+ }
+}
+
+void PlaybackVersion2Post(int client, int bot)
+{
+ if (botPlaybackPaused[bot])
+ {
+ return;
+ }
+ int size = playbackTickData[bot].Length;
+ if (playbackTick[bot] != 0 && playbackTick[bot] != (size - 1))
+ {
+ ReplayTickData currentTickData;
+ ReplayTickData prevTickData;
+ playbackTickData[bot].GetArray(playbackTick[bot], currentTickData);
+ playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData);
+
+ // TeleportEntity does not set the absolute origin and velocity so we need to do it
+ // to prevent inaccurate eye position interpolation.
+ SetEntPropVector(client, Prop_Data, "m_vecVelocity", currentTickData.velocity);
+ SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", currentTickData.velocity);
+
+ SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", currentTickData.origin);
+ SetEntPropVector(client, Prop_Data, "m_vecOrigin", currentTickData.origin);
+
+ SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[0]", currentTickData.angles[0]);
+ SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[1]", currentTickData.angles[1]);
+
+ MoveType replayMoveType = view_as<MoveType>(currentTickData.flags & RP_MOVETYPE_MASK);
+ botMoveType[bot] = replayMoveType;
+ int entityFlags = GetEntityFlags(client);
+ if (replayMoveType == MOVETYPE_WALK)
+ {
+ if (currentTickData.flags & RP_FL_ONGROUND)
+ {
+ SetEntityFlags(client, entityFlags | FL_ONGROUND);
+ botPaused[bot] = false;
+ // The bot is on the ground, so there must be a ground entity attributed to the bot.
+ int groundEnt = GetEntPropEnt(client, Prop_Send, "m_hGroundEntity");
+ if (groundEnt == -1 && botJustTeleported[bot])
+ {
+ SetEntPropFloat(client, Prop_Send, "m_flFallVelocity", 0.0);
+ float endPosition[3], mins[3], maxs[3];
+ GetEntPropVector(client, Prop_Send, "m_vecMaxs", maxs);
+ GetEntPropVector(client, Prop_Send, "m_vecMins", mins);
+ endPosition = currentTickData.origin;
+ endPosition[2] -= 2.0;
+ TR_TraceHullFilter(currentTickData.origin, endPosition, mins, maxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+
+ // This should always hit.
+ if (TR_DidHit())
+ {
+ groundEnt = TR_GetEntityIndex();
+ SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", groundEnt);
+ }
+ }
+ }
+ else
+ {
+ botJustTeleported[bot] = false;
+ }
+ }
+
+ if (currentTickData.flags & RP_UNDER_WATER)
+ {
+ SetEntityFlags(client, entityFlags | FL_INWATER);
+ }
+
+ botSpeed[bot] = GetVectorHorizontalLength(currentTickData.velocity);
+ playbackTick[bot]++;
+ }
+}
+
+// Set the bot client's GOKZ options, clan tag and name based on the loaded replay data
+static void SetBotStuff(int bot)
+{
+ if (!botInGame[bot] || !botDataLoaded[bot])
+ {
+ return;
+ }
+
+ int client = botClient[bot];
+
+ // Set its movement options just in case it could negatively affect the playback
+ GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]);
+ GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]);
+
+ // Clan tag and name
+ SetBotClanTag(bot);
+ SetBotName(bot);
+
+ // Bot takes one tick after being put in server to be able to respawn.
+ RequestFrame(RequestFrame_SetBotStuff, GetClientUserId(client));
+}
+
+public void RequestFrame_SetBotStuff(int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (!client)
+ {
+ return;
+ }
+ int bot;
+ for (bot = 0; bot <= RP_MAX_BOTS; bot++)
+ {
+ if (botClient[bot] == client)
+ {
+ break;
+ }
+ else if (bot == RP_MAX_BOTS)
+ {
+ return;
+ }
+ }
+ // Set the bot's team based on if it's NUB or PRO
+ if (botReplayType[bot] == ReplayType_Run
+ && GOKZ_GetTimeTypeEx(botTeleportsUsed[bot]) == TimeType_Pro)
+ {
+ GOKZ_JoinTeam(client, CS_TEAM_CT, .forceBroadcast = true);
+ }
+ else
+ {
+ GOKZ_JoinTeam(client, CS_TEAM_CT, .forceBroadcast = true);
+ }
+ // Set bot weapons
+ // Always start by removing the pistol and knife
+ int currentPistol = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY);
+ if (currentPistol != -1)
+ {
+ RemovePlayerItem(client, currentPistol);
+ }
+
+ int currentKnife = GetPlayerWeaponSlot(client, CS_SLOT_KNIFE);
+ if (currentKnife != -1)
+ {
+ RemovePlayerItem(client, currentKnife);
+ }
+
+ char weaponName[128];
+ // Give the bot the knife stored in the replay
+ /*
+ if (botKnife[bot] != 0)
+ {
+ CS_WeaponIDToAlias(CS_ItemDefIndexToID(botKnife[bot]), weaponName, sizeof(weaponName));
+ Format(weaponName, sizeof(weaponName), "weapon_%s", weaponName);
+ GivePlayerItem(client, weaponName);
+ }
+ else
+ {
+ GivePlayerItem(client, "weapon_knife");
+ }
+ */
+ // We are currently not doing that, as it would require us to disable the
+ // FollowCSGOServerGuidelines failsafe if the bot has a non-standard knife.
+ GivePlayerItem(client, "weapon_knife");
+
+ // Give the bot the pistol stored in the replay
+ if (botWeapon[bot] != -1)
+ {
+ CS_WeaponIDToAlias(CS_ItemDefIndexToID(botWeapon[bot]), weaponName, sizeof(weaponName));
+ Format(weaponName, sizeof(weaponName), "weapon_%s", weaponName);
+ GivePlayerItem(client, weaponName);
+ }
+
+ botCurrentTeleport[bot] = 0;
+}
+
+static void SetBotClanTag(int bot)
+{
+ char tag[MAX_NAME_LENGTH];
+
+ if (botReplayType[bot] == ReplayType_Run)
+ {
+ if (botCourse[bot] == 0)
+ {
+ // KZT PRO
+ FormatEx(tag, sizeof(tag), "%s %s",
+ gC_ModeNamesShort[botMode[bot]], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]);
+ }
+ else
+ {
+ // KZT B2 PRO
+ FormatEx(tag, sizeof(tag), "%s B%d %s",
+ gC_ModeNamesShort[botMode[bot]], botCourse[bot], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]);
+ }
+ }
+ else if (botReplayType[bot] == ReplayType_Jump)
+ {
+ // KZT LJ
+ FormatEx(tag, sizeof(tag), "%s %s",
+ gC_ModeNamesShort[botMode[bot]], gC_JumpTypesShort[botJumpType[bot]]);
+ }
+ else
+ {
+ // KZT
+ FormatEx(tag, sizeof(tag), "%s",
+ gC_ModeNamesShort[botMode[bot]]);
+ }
+
+ CS_SetClientClanTag(botClient[bot], tag);
+}
+
+static void SetBotName(int bot)
+{
+ char name[MAX_NAME_LENGTH];
+
+ if (botReplayType[bot] == ReplayType_Run)
+ {
+ // DanZay (01:23.45)
+ FormatEx(name, sizeof(name), "%s (%s)",
+ botAlias[bot], GOKZ_FormatTime(botTime[bot]));
+ }
+ else if (botReplayType[bot] == ReplayType_Jump)
+ {
+ if (botJumpBlockDistance[bot] == 0)
+ {
+ // DanZay (291.44)
+ FormatEx(name, sizeof(name), "%s (%.2f)",
+ botAlias[bot], botJumpDistance[bot]);
+ }
+ else
+ {
+ // DanZay (291.44 on 289 block)
+ FormatEx(name, sizeof(name), "%s (%.2f on %d block)",
+ botAlias[bot], botJumpDistance[bot], botJumpBlockDistance[bot]);
+ }
+ }
+ else
+ {
+ // DanZay
+ FormatEx(name, sizeof(name), "%s",
+ botAlias[bot]);
+ }
+
+ gB_HideNameChange = true;
+ SetClientName(botClient[bot], name);
+}
+
+// Returns the number of bots that are currently replaying
+static int GetBotsInUse()
+{
+ int botsInUse = 0;
+ for (int bot; bot < RP_MAX_BOTS; bot++)
+ {
+ if (botInGame[bot] && botDataLoaded[bot])
+ {
+ botsInUse++;
+ }
+ }
+ return botsInUse;
+}
+
+// Returns a bot that isn't currently replaying, or -1 if no unused bots found
+static int GetUnusedBot()
+{
+ for (int bot = 0; bot < RP_MAX_BOTS; bot++)
+ {
+ if (!botInGame[bot])
+ {
+ return bot;
+ }
+ }
+ return -1;
+}
+
+static void PlaybackSkipToTick(int bot, int tick)
+{
+ if (botReplayVersion[bot] == 1)
+ {
+ // Load in the next tick
+ float repOrigin[3], repAngles[3];
+ repOrigin[0] = playbackTickData[bot].Get(tick, 0);
+ repOrigin[1] = playbackTickData[bot].Get(tick, 1);
+ repOrigin[2] = playbackTickData[bot].Get(tick, 2);
+ repAngles[0] = playbackTickData[bot].Get(tick, 3);
+ repAngles[1] = playbackTickData[bot].Get(tick, 4);
+
+ TeleportEntity(botClient[bot], repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ }
+ else if (botReplayVersion[bot] == 2)
+ {
+ // Load in the next tick
+ ReplayTickData currentTickData;
+ playbackTickData[bot].GetArray(tick, currentTickData);
+
+ TeleportEntity(botClient[bot], currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } ));
+
+ int direction = tick < playbackTick[bot] ? -1 : 1;
+ for (int i = playbackTick[bot]; i != tick; i += direction)
+ {
+ playbackTickData[bot].GetArray(i, currentTickData);
+ if (currentTickData.flags & RP_TELEPORT_TICK)
+ {
+ botCurrentTeleport[bot] += direction;
+ }
+ }
+
+ #if defined DEBUG
+ PrintToServer("X %f \nY %f \nZ %f\nPitch %f\nYaw %f", currentTickData.origin[0], currentTickData.origin[1], currentTickData.origin[2], currentTickData.angles[0], currentTickData.angles[1]);
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK");
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER");
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP");
+ if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NONE)) PrintToServer("MOVETYPE_NONE");
+
+ if(currentTickData.flags & RP_IN_ATTACK) PrintToServer("IN_ATTACK");
+ if(currentTickData.flags & RP_IN_ATTACK2) PrintToServer("IN_ATTACK2");
+ if(currentTickData.flags & RP_IN_JUMP) PrintToServer("IN_JUMP");
+ if(currentTickData.flags & RP_IN_DUCK) PrintToServer("IN_DUCK");
+ if(currentTickData.flags & RP_IN_FORWARD) PrintToServer("IN_FORWARD");
+ if(currentTickData.flags & RP_IN_BACK) PrintToServer("IN_BACK");
+ if(currentTickData.flags & RP_IN_LEFT) PrintToServer("IN_LEFT");
+ if(currentTickData.flags & RP_IN_RIGHT) PrintToServer("IN_RIGHT");
+ if(currentTickData.flags & RP_IN_MOVELEFT) PrintToServer("IN_MOVELEFT");
+ if(currentTickData.flags & RP_IN_MOVERIGHT) PrintToServer("IN_MOVERIGHT");
+ if(currentTickData.flags & RP_IN_RELOAD) PrintToServer("IN_RELOAD");
+ if(currentTickData.flags & RP_IN_SPEED) PrintToServer("IN_SPEED");
+ if(currentTickData.flags & RP_FL_ONGROUND) PrintToServer("FL_ONGROUND");
+ if(currentTickData.flags & RP_FL_DUCKING ) PrintToServer("FL_DUCKING");
+ if(currentTickData.flags & RP_FL_SWIM) PrintToServer("FL_SWIM");
+ if(currentTickData.flags & RP_UNDER_WATER) PrintToServer("WATERLEVEL!=0");
+ if(currentTickData.flags & RP_TELEPORT_TICK) PrintToServer("TELEPORT");
+ if(currentTickData.flags & RP_TAKEOFF_TICK) PrintToServer("TAKEOFF");
+ if(currentTickData.flags & RP_HIT_PERF) PrintToServer("PERF");
+ if(currentTickData.flags & RP_SECONDARY_EQUIPPED) PrintToServer("SECONDARY_WEAPON_EQUIPPED");
+ PrintToServer("==============================================================");
+ #endif
+ }
+
+ Movement_SetMovetype(botClient[bot], MOVETYPE_NOCLIP);
+ playbackTick[bot] = tick;
+}
+
+static bool IsCurrentWeaponSecondary(int client)
+{
+ int activeWeaponEnt = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
+ int secondaryEnt = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY);
+ return activeWeaponEnt == secondaryEnt;
+}
+
+static void MakePlayerSpectate(int client, int bot)
+{
+ GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR);
+ SetEntProp(client, Prop_Send, "m_iObserverMode", 4);
+ SetEntPropEnt(client, Prop_Send, "m_hObserverTarget", bot);
+
+ int clientUserID = GetClientUserId(client);
+ DataPack data = new DataPack();
+ data.WriteCell(clientUserID);
+ data.WriteCell(GetClientUserId(bot));
+ CreateTimer(0.1, Timer_UpdateBotName, GetClientUserId(bot));
+ EnableReplayControls(client);
+}
+
+public Action Timer_UpdateBotName(Handle timer, int botUID)
+{
+ Event e = CreateEvent("spec_target_updated");
+ e.SetInt("userid", botUID);
+ e.Fire();
+ return Plugin_Continue;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-replays/recording.sp b/sourcemod/scripting/gokz-replays/recording.sp
new file mode 100644
index 0000000..babbd5e
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/recording.sp
@@ -0,0 +1,990 @@
+/*
+ Bot replay recording logic and processes.
+
+ Records data every time OnPlayerRunCmdPost is called.
+ If the player doesn't have their timer running, it keeps track
+ of the last 2 minutes of their actions. If a player is banned
+ while their timer isn't running, those 2 minutes are saved.
+ If the player has their timer running, the recording is done from
+ the beginning of the run. If the player can no longer beat their PB,
+ then the recording goes back to only keeping track of the last
+ two minutes. Upon beating their PB, a temporary binary file will be
+ written with a 'header' containing information about the run,
+ followed by the recorded tick data from OnPlayerRunCmdPost.
+ The binary file will be permanently locally saved on the server
+ if the run beats the server record.
+*/
+
+static float tickrate;
+static int preAndPostRunTickCount;
+static int maxCheaterReplayTicks;
+static int recordingIndex[MAXPLAYERS + 1];
+static float playerSensitivity[MAXPLAYERS + 1];
+static float playerMYaw[MAXPLAYERS + 1];
+static bool isTeleportTick[MAXPLAYERS + 1];
+static ReplaySaveState replaySaveState[MAXPLAYERS + 1];
+static bool recordingPaused[MAXPLAYERS + 1];
+static bool postRunRecording[MAXPLAYERS + 1];
+static ArrayList recordedRecentData[MAXPLAYERS + 1];
+static ArrayList recordedRunData[MAXPLAYERS + 1];
+static ArrayList recordedPostRunData[MAXPLAYERS + 1];
+static Handle runningRunBreatherTimer[MAXPLAYERS + 1];
+static ArrayList runningJumpstatTimers[MAXPLAYERS + 1];
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_Recording()
+{
+ CreateReplaysDirectory(gC_CurrentMap);
+ tickrate = 1/GetTickInterval();
+ preAndPostRunTickCount = RoundToZero(RP_PLAYBACK_BREATHER_TIME * tickrate);
+ maxCheaterReplayTicks = RoundToCeil(RP_MAX_CHEATER_REPLAY_LENGTH * tickrate);
+}
+
+void OnClientPutInServer_Recording(int client)
+{
+ ClearClientRecordingState(client);
+}
+
+void OnClientAuthorized_Recording(int client)
+{
+ // Apparently the client isn't valid yet here, so we can't check for that!
+ if(!IsFakeClient(client))
+ {
+ // Create directory path for player if not exists
+ char replayPath[PLATFORM_MAX_PATH];
+ BuildPath(Path_SM, replayPath, sizeof(replayPath), "%s/%d", RP_DIRECTORY_JUMPS, GetSteamAccountID(client));
+ if (!DirExists(replayPath))
+ {
+ CreateDirectory(replayPath, 511);
+ }
+ BuildPath(Path_SM, replayPath, sizeof(replayPath), "%s/%d/%s", RP_DIRECTORY_JUMPS, GetSteamAccountID(client), RP_DIRECTORY_BLOCKJUMPS);
+ if (!DirExists(replayPath))
+ {
+ CreateDirectory(replayPath, 511);
+ }
+ }
+}
+
+void OnClientDisconnect_Recording(int client)
+{
+ // Stop exceptions if OnClientPutInServer was never ran for this client id.
+ // As long as the arrays aren't null we'll be fine.
+ if (runningJumpstatTimers[client] == null)
+ {
+ return;
+ }
+
+ // Trigger all timers early
+ if(!IsFakeClient(client))
+ {
+ if (runningRunBreatherTimer[client] != INVALID_HANDLE)
+ {
+ TriggerTimer(runningRunBreatherTimer[client], false);
+ }
+
+ // We have to clone the array because the timer callback removes the timer
+ // from the array we're running over, and doing weird tricks is scary.
+ ArrayList timers = runningJumpstatTimers[client].Clone();
+ for (int i = 0; i < timers.Length; i++)
+ {
+ Handle timer = timers.Get(i);
+ TriggerTimer(timer, false);
+ }
+ delete timers;
+ }
+
+ ClearClientRecordingState(client);
+}
+
+void OnPlayerRunCmdPost_Recording(int client, int buttons, int tickCount, const float vel[3], const int mouse[2])
+{
+ if (!IsValidClient(client) || IsFakeClient(client) || !IsPlayerAlive(client) || recordingPaused[client])
+ {
+ return;
+ }
+
+ ReplayTickData tickData;
+
+ Movement_GetOrigin(client, tickData.origin);
+
+ tickData.mouse = mouse;
+ tickData.vel = vel;
+ Movement_GetVelocity(client, tickData.velocity);
+ Movement_GetEyeAngles(client, tickData.angles);
+ tickData.flags = EncodePlayerFlags(client, buttons, tickCount);
+ tickData.packetsPerSecond = GetClientAvgPackets(client, NetFlow_Incoming);
+ tickData.laggedMovementValue = GetEntPropFloat(client, Prop_Send, "m_flLaggedMovementValue");
+ tickData.buttonsForced = GetEntProp(client, Prop_Data, "m_afButtonForced");
+
+ // HACK: Reset teleport tick marker. Too bad!
+ if (isTeleportTick[client])
+ {
+ isTeleportTick[client] = false;
+ }
+
+ if (replaySaveState[client] != ReplaySave_Disabled)
+ {
+ int runTick = GetArraySize(recordedRunData[client]);
+ if (runTick < RP_MAX_DURATION)
+ {
+ // Resize might fail if the timer exceed the max duration,
+ // as it is not guaranteed to allocate more than 1GB of contiguous memory,
+ // causing mass lag spikes that kick everyone out of the server.
+ // We can still attempt to save the rest of the recording though.
+ recordedRunData[client].Resize(runTick + 1);
+ recordedRunData[client].SetArray(runTick, tickData);
+ }
+ }
+ if (postRunRecording[client])
+ {
+ int tick = GetArraySize(recordedPostRunData[client]);
+ if (tick < RP_MAX_DURATION)
+ {
+ recordedPostRunData[client].Resize(tick + 1);
+ recordedPostRunData[client].SetArray(tick, tickData);
+ }
+ }
+
+ int tick = recordingIndex[client];
+ if (recordedRecentData[client].Length < maxCheaterReplayTicks)
+ {
+ recordedRecentData[client].Resize(recordedRecentData[client].Length + 1);
+ recordingIndex[client] = recordingIndex[client] + 1 == maxCheaterReplayTicks ? 0 : recordingIndex[client] + 1;
+ }
+ else
+ {
+ recordingIndex[client] = RecordingIndexAdd(client, 1);
+ }
+
+ recordedRecentData[client].SetArray(tick, tickData);
+}
+
+Action GOKZ_OnTimerStart_Recording(int client)
+{
+ // Hack to fix an exception when starting the timer on the very
+ // first tick after loading the plugin.
+ if (recordedRecentData[client].Length == 0)
+ {
+ return Plugin_Handled;
+ }
+
+ return Plugin_Continue;
+}
+
+void GOKZ_OnTimerStart_Post_Recording(int client)
+{
+ replaySaveState[client] = ReplaySave_Local;
+ StartRunRecording(client);
+}
+
+void GOKZ_OnTimerEnd_Recording(int client, int course, float time, int teleportsUsed)
+{
+ if (replaySaveState[client] == ReplaySave_Disabled)
+ {
+ return;
+ }
+
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(course);
+ data.WriteFloat(time);
+ data.WriteCell(teleportsUsed);
+ data.WriteCell(replaySaveState[client]);
+ // The previous run breather still did not finish, end it now or
+ // we will start overwriting the data.
+ if (runningRunBreatherTimer[client] != INVALID_HANDLE)
+ {
+ TriggerTimer(runningRunBreatherTimer[client], false);
+ }
+
+ replaySaveState[client] = ReplaySave_Disabled;
+ postRunRecording[client] = true;
+
+ // Swap recordedRunData and recordedPostRunData.
+ // This lets new runs start immediately, before the post-run breather is
+ // finished recording.
+ ArrayList tmp = recordedPostRunData[client];
+ recordedPostRunData[client] = recordedRunData[client];
+ recordedRunData[client] = tmp;
+ recordedRunData[client].Clear();
+
+ runningRunBreatherTimer[client] = CreateTimer(RP_PLAYBACK_BREATHER_TIME, Timer_EndRecording, data);
+ if (runningRunBreatherTimer[client] == INVALID_HANDLE)
+ {
+ LogError("Could not create a timer so can't end the run replay recording");
+ }
+}
+
+public Action Timer_EndRecording(Handle timer, DataPack data)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int course = data.ReadCell();
+ float time = data.ReadFloat();
+ int teleportsUsed = data.ReadCell();
+ ReplaySaveState saveState = data.ReadCell();
+ delete data;
+
+ // The client left after the run was done but before the post-run
+ // breather had the chance to finish. This should not happen, as we
+ // trigger all running timers on disconnect.
+ if (!IsValidClient(client))
+ {
+ return Plugin_Stop;
+ }
+
+ runningRunBreatherTimer[client] = INVALID_HANDLE;
+ postRunRecording[client] = false;
+
+ if (gB_GOKZLocalDB && GOKZ_DB_IsCheater(client))
+ {
+ // Replay might be submitted globally, but will not be saved locally.
+ saveState = ReplaySave_Temp;
+ }
+
+ char path[PLATFORM_MAX_PATH];
+ if (SaveRecordingOfRun(path, client, course, time, teleportsUsed, saveState == ReplaySave_Temp))
+ {
+ Call_OnTimerEnd_Post(client, path, course, time, teleportsUsed);
+ }
+ else
+ {
+ Call_OnTimerEnd_Post(client, "", course, time, teleportsUsed);
+ }
+
+ return Plugin_Stop;
+}
+
+void GOKZ_OnPause_Recording(int client)
+{
+ PauseRecording(client);
+}
+
+void GOKZ_OnResume_Recording(int client)
+{
+ ResumeRecording(client);
+}
+
+void GOKZ_OnTimerStopped_Recording(int client)
+{
+ replaySaveState[client] = ReplaySave_Disabled;
+}
+
+void GOKZ_OnCountedTeleport_Recording(int client)
+{
+ if (gB_NubRecordMissed[client])
+ {
+ replaySaveState[client] = ReplaySave_Disabled;
+ }
+
+ isTeleportTick[client] = true;
+}
+
+void GOKZ_LR_OnRecordMissed_Recording(int client, int recordType)
+{
+ if (replaySaveState[client] == ReplaySave_Disabled)
+ {
+ return;
+ }
+ // If missed PRO record or both records, then can no longer beat a server record
+ if (recordType == RecordType_NubAndPro || recordType == RecordType_Pro)
+ {
+ replaySaveState[client] = ReplaySave_Temp;
+ }
+
+ // If on a NUB run and missed NUB record, then can no longer beat a server record
+ // Otherwise wait to see if they teleport before stopping the recording
+ if (recordType == RecordType_Nub)
+ {
+ if (GOKZ_GetTeleportCount(client) > 0)
+ {
+ replaySaveState[client] = ReplaySave_Temp;
+ }
+ }
+}
+
+public void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType)
+{
+ if (replaySaveState[client] == ReplaySave_Disabled)
+ {
+ return;
+ }
+ // If missed PRO record or both records, then can no longer beat PB
+ if (recordType == RecordType_NubAndPro || recordType == RecordType_Pro)
+ {
+ replaySaveState[client] = ReplaySave_Disabled;
+ }
+
+ // If on a NUB run and missed NUB record, then can no longer beat PB
+ // Otherwise wait to see if they teleport before stopping the recording
+ if (recordType == RecordType_Nub)
+ {
+ if (GOKZ_GetTeleportCount(client) > 0)
+ {
+ replaySaveState[client] = ReplaySave_Disabled;
+ }
+ }
+}
+
+void GOKZ_AC_OnPlayerSuspected_Recording(int client, ACReason reason)
+{
+ SaveRecordingOfCheater(client, reason);
+}
+
+void GOKZ_DB_OnJumpstatPB_Recording(int client, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(jumptype);
+ data.WriteFloat(distance);
+ data.WriteCell(block);
+ data.WriteCell(strafes);
+ data.WriteFloat(sync);
+ data.WriteFloat(pre);
+ data.WriteFloat(max);
+ data.WriteCell(airtime);
+
+ Handle timer = CreateTimer(RP_PLAYBACK_BREATHER_TIME, SaveJump, data);
+ if (timer != INVALID_HANDLE)
+ {
+ runningJumpstatTimers[client].Push(timer);
+ }
+ else
+ {
+ LogError("Could not create a timer so can't save jumpstat pb replay");
+ }
+}
+
+public Action SaveJump(Handle timer, DataPack data)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int jumptype = data.ReadCell();
+ float distance = data.ReadFloat();
+ int block = data.ReadCell();
+ int strafes = data.ReadCell();
+ float sync = data.ReadFloat();
+ float pre = data.ReadFloat();
+ float max = data.ReadFloat();
+ int airtime = data.ReadCell();
+ delete data;
+
+ // The client left after the jump was done but before the post-jump
+ // breather had the chance to finish. This should not happen, as we
+ // trigger all running timers on disconnect.
+ if (!IsValidClient(client))
+ {
+ return Plugin_Stop;
+ }
+
+ RemoveFromRunningTimers(client, timer);
+
+ SaveRecordingOfJump(client, jumptype, distance, block, strafes, sync, pre, max, airtime);
+ return Plugin_Stop;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void ClearClientRecordingState(int client)
+{
+ recordingIndex[client] = 0;
+ playerSensitivity[client] = -1.0;
+ playerMYaw[client] = -1.0;
+ isTeleportTick[client] = false;
+ replaySaveState[client] = ReplaySave_Disabled;
+ recordingPaused[client] = false;
+ postRunRecording[client] = false;
+ runningRunBreatherTimer[client] = INVALID_HANDLE;
+
+ if (recordedRecentData[client] == null)
+ recordedRecentData[client] = new ArrayList(sizeof(ReplayTickData));
+
+ if (recordedRunData[client] == null)
+ recordedRunData[client] = new ArrayList(sizeof(ReplayTickData));
+
+ if (recordedPostRunData[client] == null)
+ recordedPostRunData[client] = new ArrayList(sizeof(ReplayTickData));
+
+ if (runningJumpstatTimers[client] == null)
+ runningJumpstatTimers[client] = new ArrayList();
+
+ recordedRecentData[client].Clear();
+ recordedRunData[client].Clear();
+ recordedPostRunData[client].Clear();
+ runningJumpstatTimers[client].Clear();
+}
+
+static void StartRunRecording(int client)
+{
+ if (IsFakeClient(client))
+ {
+ return;
+ }
+
+ QueryClientConVar(client, "sensitivity", SensitivityCheck, client);
+ QueryClientConVar(client, "m_yaw", MYAWCheck, client);
+
+ DiscardRecording(client);
+ ResumeRecording(client);
+
+ // Copy pre data
+ int index;
+ recordedRunData[client].Resize(preAndPostRunTickCount);
+ if (recordedRecentData[client].Length < preAndPostRunTickCount)
+ {
+ index = recordingIndex[client] - preAndPostRunTickCount;
+ }
+ else
+ {
+ index = RecordingIndexAdd(client, -preAndPostRunTickCount);
+ }
+ for (int i = 0; i < preAndPostRunTickCount; i++)
+ {
+ ReplayTickData tickData;
+ if (index < 0)
+ {
+ recordedRecentData[client].GetArray(0, tickData);
+ recordedRunData[client].SetArray(i, tickData);
+ index += 1;
+ }
+ else
+ {
+ recordedRecentData[client].GetArray(index, tickData);
+ recordedRunData[client].SetArray(i, tickData);
+ index = RecordingIndexAdd(client, -preAndPostRunTickCount + i + 1);
+ }
+ }
+}
+
+static void DiscardRecording(int client)
+{
+ recordedRunData[client].Clear();
+ Call_OnReplayDiscarded(client);
+}
+
+static void PauseRecording(int client)
+{
+ recordingPaused[client] = true;
+}
+
+static void ResumeRecording(int client)
+{
+ recordingPaused[client] = false;
+}
+
+static bool SaveRecordingOfRun(char replayPath[PLATFORM_MAX_PATH], int client, int course, float time, int teleportsUsed, bool temp)
+{
+ // Prepare data
+ int timeType = GOKZ_GetTimeTypeEx(teleportsUsed);
+
+ // Create and fill General Header
+ GeneralReplayHeader generalHeader;
+ FillGeneralHeader(generalHeader, client, ReplayType_Run, recordedPostRunData[client].Length);
+
+ // Create and fill Run Header
+ RunReplayHeader runHeader;
+ runHeader.time = time;
+ runHeader.course = course;
+ runHeader.teleportsUsed = teleportsUsed;
+
+ // Build path and create/overwrite associated file
+ FormatRunReplayPath(replayPath, sizeof(replayPath), course, generalHeader.mode, generalHeader.style, timeType, temp);
+ if (FileExists(replayPath))
+ {
+ DeleteFile(replayPath);
+ }
+ else if (!temp)
+ {
+ AddToReplayInfoCache(course, generalHeader.mode, generalHeader.style, timeType);
+ SortReplayInfoCache();
+ }
+
+ File file = OpenFile(replayPath, "wb");
+ if (file == null)
+ {
+ LogError("Failed to create/open replay file to write to: \"%s\".", replayPath);
+ return false;
+ }
+
+ WriteGeneralHeader(file, generalHeader);
+
+ // Write run header
+ file.WriteInt32(view_as<int>(runHeader.time));
+ file.WriteInt8(runHeader.course);
+ file.WriteInt32(runHeader.teleportsUsed);
+
+ WriteTickData(file, client, ReplayType_Run);
+
+ delete file;
+ // If there is no plugin that wants to take over the replay file, we will delete it ourselves.
+ if (Call_OnReplaySaved(client, ReplayType_Run, gC_CurrentMap, course, timeType, time, replayPath, temp) == Plugin_Continue && temp)
+ {
+ DeleteFile(replayPath);
+ }
+
+ return true;
+}
+
+static bool SaveRecordingOfCheater(int client, ACReason reason)
+{
+ // Create and fill general header
+ GeneralReplayHeader generalHeader;
+ FillGeneralHeader(generalHeader, client, ReplayType_Cheater, recordedRecentData[client].Length);
+
+ // Create and fill cheater header
+ CheaterReplayHeader cheaterHeader;
+ cheaterHeader.ACReason = reason;
+
+ //Build path and create/overwrite associated file
+ char replayPath[PLATFORM_MAX_PATH];
+ FormatCheaterReplayPath(replayPath, sizeof(replayPath), client, generalHeader.mode, generalHeader.style);
+
+ File file = OpenFile(replayPath, "wb");
+ if (file == null)
+ {
+ LogError("Failed to create/open replay file to write to: \"%s\".", replayPath);
+ return false;
+ }
+
+ WriteGeneralHeader(file, generalHeader);
+ file.WriteInt8(view_as<int>(cheaterHeader.ACReason));
+ WriteTickData(file, client, ReplayType_Cheater);
+
+ delete file;
+
+ return true;
+}
+
+static bool SaveRecordingOfJump(int client, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime)
+{
+ // Just cause I know how buggy jumpstats can be
+ int airtimeTicks = RoundToNearest((float(airtime) / GOKZ_DB_JS_AIRTIME_PRECISION) * tickrate);
+ if (airtimeTicks + 2 * preAndPostRunTickCount >= maxCheaterReplayTicks)
+ {
+ LogError("WARNING: Invalid airtime (this is probably a bugged jump, please report it!).");
+ return false;
+ }
+
+ // Create and fill general header
+ GeneralReplayHeader generalHeader;
+ FillGeneralHeader(generalHeader, client, ReplayType_Jump, 2 * preAndPostRunTickCount + airtimeTicks);
+
+ // Create and fill jump header
+ JumpReplayHeader jumpHeader;
+ FillJumpHeader(jumpHeader, jumptype, distance, block, strafes, sync, pre, max, airtime);
+
+ // Make sure the client is authenticated
+ if (GetSteamAccountID(client) == 0)
+ {
+ LogError("Failed to save jump, client is not authenticated.");
+ return false;
+ }
+
+ // Build path and create/overwrite associated file
+ char replayPath[PLATFORM_MAX_PATH];
+ if (block > 0)
+ {
+ FormatBlockJumpReplayPath(replayPath, sizeof(replayPath), client, block, jumpHeader.jumpType, generalHeader.mode, generalHeader.style);
+ }
+ else
+ {
+ FormatJumpReplayPath(replayPath, sizeof(replayPath), client, jumpHeader.jumpType, generalHeader.mode, generalHeader.style);
+ }
+
+ File file = OpenFile(replayPath, "wb");
+ if (file == null)
+ {
+ LogError("Failed to create/open replay file to write to: \"%s\".", replayPath);
+ delete file;
+ return false;
+ }
+
+ WriteGeneralHeader(file, generalHeader);
+ WriteJumpHeader(file, jumpHeader);
+ WriteTickData(file, client, ReplayType_Jump, airtimeTicks);
+
+ delete file;
+
+ return true;
+}
+
+static void FillGeneralHeader(GeneralReplayHeader generalHeader, int client, int replayType, int tickCount)
+{
+ // Prepare data
+ int mode = GOKZ_GetCoreOption(client, Option_Mode);
+ int style = GOKZ_GetCoreOption(client, Option_Style);
+
+ // Fill general header
+ generalHeader.magicNumber = RP_MAGIC_NUMBER;
+ generalHeader.formatVersion = RP_FORMAT_VERSION;
+ generalHeader.replayType = replayType;
+ generalHeader.gokzVersion = GOKZ_VERSION;
+ generalHeader.mapName = gC_CurrentMap;
+ generalHeader.mapFileSize = gI_CurrentMapFileSize;
+ generalHeader.serverIP = FindConVar("hostip").IntValue;
+ generalHeader.timestamp = GetTime();
+ GetClientName(client, generalHeader.playerAlias, sizeof(GeneralReplayHeader::playerAlias));
+ generalHeader.playerSteamID = GetSteamAccountID(client);
+ generalHeader.mode = mode;
+ generalHeader.style = style;
+ generalHeader.playerSensitivity = playerSensitivity[client];
+ generalHeader.playerMYaw = playerMYaw[client];
+ generalHeader.tickrate = tickrate;
+ generalHeader.tickCount = tickCount;
+ generalHeader.equippedWeapon = GetPlayerWeaponSlotDefIndex(client, CS_SLOT_SECONDARY);
+ generalHeader.equippedKnife = GetPlayerWeaponSlotDefIndex(client, CS_SLOT_KNIFE);
+}
+
+static void FillJumpHeader(JumpReplayHeader jumpHeader, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime)
+{
+ jumpHeader.jumpType = jumptype;
+ jumpHeader.distance = distance;
+ jumpHeader.blockDistance = block;
+ jumpHeader.strafeCount = strafes;
+ jumpHeader.sync = sync;
+ jumpHeader.pre = pre;
+ jumpHeader.max = max;
+ jumpHeader.airtime = airtime;
+}
+
+static void WriteGeneralHeader(File file, GeneralReplayHeader generalHeader)
+{
+ file.WriteInt32(generalHeader.magicNumber);
+ file.WriteInt8(generalHeader.formatVersion);
+ file.WriteInt8(generalHeader.replayType);
+ file.WriteInt8(strlen(generalHeader.gokzVersion));
+ file.WriteString(generalHeader.gokzVersion, false);
+ file.WriteInt8(strlen(generalHeader.mapName));
+ file.WriteString(generalHeader.mapName, false);
+ file.WriteInt32(generalHeader.mapFileSize);
+ file.WriteInt32(generalHeader.serverIP);
+ file.WriteInt32(generalHeader.timestamp);
+ file.WriteInt8(strlen(generalHeader.playerAlias));
+ file.WriteString(generalHeader.playerAlias, false);
+ file.WriteInt32(generalHeader.playerSteamID);
+ file.WriteInt8(generalHeader.mode);
+ file.WriteInt8(generalHeader.style);
+ file.WriteInt32(view_as<int>(generalHeader.playerSensitivity));
+ file.WriteInt32(view_as<int>(generalHeader.playerMYaw));
+ file.WriteInt32(view_as<int>(generalHeader.tickrate));
+ file.WriteInt32(generalHeader.tickCount);
+ file.WriteInt32(generalHeader.equippedWeapon);
+ file.WriteInt32(generalHeader.equippedKnife);
+}
+
+static void WriteJumpHeader(File file, JumpReplayHeader jumpHeader)
+{
+ file.WriteInt8(jumpHeader.jumpType);
+ file.WriteInt32(view_as<int>(jumpHeader.distance));
+ file.WriteInt32(jumpHeader.blockDistance);
+ file.WriteInt8(jumpHeader.strafeCount);
+ file.WriteInt32(view_as<int>(jumpHeader.sync));
+ file.WriteInt32(view_as<int>(jumpHeader.pre));
+ file.WriteInt32(view_as<int>(jumpHeader.max));
+ file.WriteInt32((jumpHeader.airtime));
+}
+
+static void WriteTickData(File file, int client, int replayType, int airtime = 0)
+{
+ ReplayTickData tickData;
+ ReplayTickData prevTickData;
+ bool isFirstTick = true;
+ switch(replayType)
+ {
+ case ReplayType_Run:
+ {
+ for (int i = 0; i < recordedPostRunData[client].Length; i++)
+ {
+ recordedPostRunData[client].GetArray(i, tickData);
+ recordedPostRunData[client].GetArray(IntMax(0, i-1), prevTickData);
+ WriteTickDataToFile(file, isFirstTick, tickData, prevTickData);
+ isFirstTick = false;
+ }
+ }
+ case ReplayType_Cheater:
+ {
+ for (int i = 0; i < recordedRecentData[client].Length; i++)
+ {
+ int rollingI = RecordingIndexAdd(client, i);
+ recordedRecentData[client].GetArray(rollingI, tickData);
+ recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData);
+ WriteTickDataToFile(file, isFirstTick, tickData, prevTickData);
+ isFirstTick = false;
+ }
+
+ }
+ case ReplayType_Jump:
+ {
+ int replayLength = 2 * preAndPostRunTickCount + airtime;
+ for (int i = 0; i < replayLength; i++)
+ {
+ int rollingI = RecordingIndexAdd(client, i - replayLength);
+ recordedRecentData[client].GetArray(rollingI, tickData);
+ recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData);
+ WriteTickDataToFile(file, isFirstTick, tickData, prevTickData);
+ isFirstTick = false;
+ }
+ }
+ }
+}
+
+static void WriteTickDataToFile(File file, bool isFirstTick, ReplayTickData tickDataStruct, ReplayTickData prevTickDataStruct)
+{
+ any tickData[RP_V2_TICK_DATA_BLOCKSIZE];
+ any prevTickData[RP_V2_TICK_DATA_BLOCKSIZE];
+ TickDataToArray(tickDataStruct, tickData);
+ TickDataToArray(prevTickDataStruct, prevTickData);
+
+ int deltaFlags = (1 << RPDELTA_DELTAFLAGS);
+ if (isFirstTick)
+ {
+ // NOTE: Set every bit to 1 until RP_V2_TICK_DATA_BLOCKSIZE.
+ deltaFlags = (1 << (RP_V2_TICK_DATA_BLOCKSIZE)) - 1;
+ }
+ else
+ {
+ // NOTE: Test tickData against prevTickData for differences.
+ for (int i = 1; i < sizeof(tickData); i++)
+ {
+ // If the bits in tickData[i] are different to prevTickData[i], then
+ // set the corresponding bitflag.
+ if (tickData[i] ^ prevTickData[i])
+ {
+ deltaFlags |= (1 << i);
+ }
+ }
+ }
+
+ file.WriteInt32(deltaFlags);
+ // NOTE: write only data that has changed since the previous tick.
+ for (int i = 1; i < sizeof(tickData); i++)
+ {
+ int currentFlag = (1 << i);
+ if (deltaFlags & currentFlag)
+ {
+ file.WriteInt32(tickData[i]);
+ }
+ }
+}
+
+static void FormatRunReplayPath(char[] buffer, int maxlength, int course, int mode, int style, int timeType, bool tempPath)
+{
+ // Use GetEngineTime to prevent accidental replay overrides.
+ // Technically it would still be possible to override this file by accident,
+ // if somehow the server restarts to this exact map and course,
+ // and this function is run at the exact same time, but that is extremely unlikely.
+ // Also by then this file should have already been deleted.
+ char tempTimeString[32];
+ Format(tempTimeString, sizeof(tempTimeString), "%f_", GetEngineTime());
+ BuildPath(Path_SM, buffer, maxlength,
+ "%s/%s/%s%d_%s_%s_%s.%s",
+ tempPath ? RP_DIRECTORY_RUNS_TEMP : RP_DIRECTORY_RUNS,
+ gC_CurrentMap,
+ tempPath ? tempTimeString : "",
+ course,
+ gC_ModeNamesShort[mode],
+ gC_StyleNamesShort[style],
+ gC_TimeTypeNames[timeType],
+ RP_FILE_EXTENSION);
+}
+
+static void FormatCheaterReplayPath(char[] buffer, int maxlength, int client, int mode, int style)
+{
+ BuildPath(Path_SM, buffer, maxlength,
+ "%s/%d_%s_%d_%s_%s.%s",
+ RP_DIRECTORY_CHEATERS,
+ GetSteamAccountID(client),
+ gC_CurrentMap,
+ GetTime(),
+ gC_ModeNamesShort[mode],
+ gC_StyleNamesShort[style],
+ RP_FILE_EXTENSION);
+}
+
+static void FormatJumpReplayPath(char[] buffer, int maxlength, int client, int jumpType, int mode, int style)
+{
+ BuildPath(Path_SM, buffer, maxlength,
+ "%s/%d/%d_%s_%s.%s",
+ RP_DIRECTORY_JUMPS,
+ GetSteamAccountID(client),
+ jumpType,
+ gC_ModeNamesShort[mode],
+ gC_StyleNamesShort[style],
+ RP_FILE_EXTENSION);
+}
+
+static void FormatBlockJumpReplayPath(char[] buffer, int maxlength, int client, int block, int jumpType, int mode, int style)
+{
+ BuildPath(Path_SM, buffer, maxlength,
+ "%s/%d/%s/%d_%d_%s_%s.%s",
+ RP_DIRECTORY_JUMPS,
+ GetSteamAccountID(client),
+ RP_DIRECTORY_BLOCKJUMPS,
+ jumpType,
+ block,
+ gC_ModeNamesShort[mode],
+ gC_StyleNamesShort[style],
+ RP_FILE_EXTENSION);
+}
+
+static int EncodePlayerFlags(int client, int buttons, int tickCount)
+{
+ int flags = 0;
+ MoveType movetype = Movement_GetMovetype(client);
+ int clientFlags = GetEntityFlags(client);
+
+ flags = view_as<int>(movetype) & RP_MOVETYPE_MASK;
+
+ SetKthBit(flags, 4, IsBitSet(buttons, IN_ATTACK));
+ SetKthBit(flags, 5, IsBitSet(buttons, IN_ATTACK2));
+ SetKthBit(flags, 6, IsBitSet(buttons, IN_JUMP));
+ SetKthBit(flags, 7, IsBitSet(buttons, IN_DUCK));
+ SetKthBit(flags, 8, IsBitSet(buttons, IN_FORWARD));
+ SetKthBit(flags, 9, IsBitSet(buttons, IN_BACK));
+ SetKthBit(flags, 10, IsBitSet(buttons, IN_LEFT));
+ SetKthBit(flags, 11, IsBitSet(buttons, IN_RIGHT));
+ SetKthBit(flags, 12, IsBitSet(buttons, IN_MOVELEFT));
+ SetKthBit(flags, 13, IsBitSet(buttons, IN_MOVERIGHT));
+ SetKthBit(flags, 14, IsBitSet(buttons, IN_RELOAD));
+ SetKthBit(flags, 15, IsBitSet(buttons, IN_SPEED));
+ SetKthBit(flags, 16, IsBitSet(buttons, IN_USE));
+ SetKthBit(flags, 17, IsBitSet(buttons, IN_BULLRUSH));
+ SetKthBit(flags, 18, IsBitSet(clientFlags, FL_ONGROUND));
+ SetKthBit(flags, 19, IsBitSet(clientFlags, FL_DUCKING));
+ SetKthBit(flags, 20, IsBitSet(clientFlags, FL_SWIM));
+
+ SetKthBit(flags, 21, GetEntProp(client, Prop_Data, "m_nWaterLevel") != 0);
+
+ SetKthBit(flags, 22, isTeleportTick[client]);
+ SetKthBit(flags, 23, Movement_GetTakeoffTick(client) == tickCount);
+ SetKthBit(flags, 24, GOKZ_GetHitPerf(client));
+ SetKthBit(flags, 25, IsCurrentWeaponSecondary(client));
+
+ return flags;
+}
+
+// Function to set the bitNum bit in integer to value
+static void SetKthBit(int &number, int offset, bool value)
+{
+ int intValue = value ? 1 : 0;
+ number |= intValue << offset;
+}
+
+static bool IsBitSet(int number, int checkBit)
+{
+ return (number & checkBit) ? true : false;
+}
+
+static int GetPlayerWeaponSlotDefIndex(int client, int slot)
+{
+ int ent = GetPlayerWeaponSlot(client, slot);
+
+ // Nothing equipped in the slot
+ if (ent == -1)
+ {
+ return -1;
+ }
+
+ return GetEntProp(ent, Prop_Send, "m_iItemDefinitionIndex");
+}
+
+static bool IsCurrentWeaponSecondary(int client)
+{
+ int activeWeaponEnt = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon");
+ int secondaryEnt = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY);
+ return activeWeaponEnt == secondaryEnt;
+}
+
+static void CreateReplaysDirectory(const char[] map)
+{
+ char path[PLATFORM_MAX_PATH];
+
+ // Create parent replay directory
+ BuildPath(Path_SM, path, sizeof(path), RP_DIRECTORY);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+
+ // Create maps parent replay directory
+ BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_RUNS);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+
+
+ // Create maps replay directory
+ BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS, map);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+
+ // Create maps parent replay directory
+ BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_RUNS_TEMP);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+
+
+ // Create maps replay directory
+ BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS_TEMP, map);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+
+ // Create cheaters replay directory
+ BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_CHEATERS);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+
+ // Create jumps parent replay directory
+ BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_JUMPS);
+ if (!DirExists(path))
+ {
+ CreateDirectory(path, 511);
+ }
+}
+
+public void MYAWCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value)
+{
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ playerMYaw[client] = StringToFloat(cvarValue);
+ }
+}
+
+public void SensitivityCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value)
+{
+ if (IsValidClient(client) && !IsFakeClient(client))
+ {
+ playerSensitivity[client] = StringToFloat(cvarValue);
+ }
+}
+
+static int RecordingIndexAdd(int client, int offset)
+{
+ int index = recordingIndex[client] + offset;
+ if (index < 0)
+ {
+ index += recordedRecentData[client].Length;
+ }
+ return index % recordedRecentData[client].Length;
+}
+
+static void RemoveFromRunningTimers(int client, Handle timerToRemove)
+{
+ int index = runningJumpstatTimers[client].FindValue(timerToRemove);
+ if (index != -1)
+ {
+ runningJumpstatTimers[client].Erase(index);
+ }
+}
diff --git a/sourcemod/scripting/gokz-replays/replay_cache.sp b/sourcemod/scripting/gokz-replays/replay_cache.sp
new file mode 100644
index 0000000..83f36d0
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/replay_cache.sp
@@ -0,0 +1,176 @@
+/*
+ Cached info about the map's available replay bots stored in an ArrayList.
+*/
+
+
+
+// =====[ PUBLIC ]=====
+
+// Adds a replay to the cache
+void AddToReplayInfoCache(int course, int mode, int style, int timeType)
+{
+ int index = g_ReplayInfoCache.Length;
+ g_ReplayInfoCache.Resize(index + 1);
+ g_ReplayInfoCache.Set(index, course, 0);
+ g_ReplayInfoCache.Set(index, mode, 1);
+ g_ReplayInfoCache.Set(index, style, 2);
+ g_ReplayInfoCache.Set(index, timeType, 3);
+}
+
+// Use this to sort the cache after finished adding to it
+void SortReplayInfoCache()
+{
+ g_ReplayInfoCache.SortCustom(SortFunc_ReplayInfoCache);
+}
+
+public int SortFunc_ReplayInfoCache(int index1, int index2, Handle array, Handle hndl)
+{
+ // Do not expect any indexes to be 'equal'
+ int replayInfo1[RP_CACHE_BLOCKSIZE], replayInfo2[RP_CACHE_BLOCKSIZE];
+ g_ReplayInfoCache.GetArray(index1, replayInfo1);
+ g_ReplayInfoCache.GetArray(index2, replayInfo2);
+
+ // Compare courses - lower course number goes first
+ if (replayInfo1[0] < replayInfo2[0])
+ {
+ return -1;
+ }
+ else if (replayInfo1[0] > replayInfo2[0])
+ {
+ return 1;
+ }
+ // Same course, so compare mode
+ else if (replayInfo1[1] < replayInfo2[1])
+ {
+ return -1;
+ }
+ else if (replayInfo1[1] > replayInfo2[1])
+ {
+ return 1;
+ }
+ // Same course and mode, so compare style
+ else if (replayInfo1[2] < replayInfo2[2])
+ {
+ return -1;
+ }
+ else if (replayInfo1[2] > replayInfo2[2])
+ {
+ return 1;
+ }
+ // Same course, mode and style so compare time type, assuming can't be identical
+ else if (replayInfo1[3] == TimeType_Pro)
+ {
+ return 1;
+ }
+ return -1;
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_ReplayCache()
+{
+ if (g_ReplayInfoCache == null)
+ {
+ g_ReplayInfoCache = new ArrayList(RP_CACHE_BLOCKSIZE, 0);
+ }
+ else
+ {
+ g_ReplayInfoCache.Clear();
+ }
+
+ char path[PLATFORM_MAX_PATH];
+ BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS, gC_CurrentMap);
+ DirectoryListing dir = OpenDirectory(path);
+
+ // We want to find files that look like "0_KZT_NRM_PRO.rec"
+ char file[PLATFORM_MAX_PATH], pieces[4][16];
+ int length, dotpos, course, mode, style, timeType;
+
+ while (dir.GetNext(file, sizeof(file)))
+ {
+ // Some credit to Influx Timer - https://github.com/TotallyMehis/Influx-Timer
+
+ // Check file extension
+ length = strlen(file);
+ dotpos = 0;
+ for (int i = 0; i < length; i++)
+ {
+ if (file[i] == '.')
+ {
+ dotpos = i;
+ }
+ }
+ if (!StrEqual(file[dotpos + 1], RP_FILE_EXTENSION, false))
+ {
+ continue;
+ }
+
+ // Remove file extension
+ Format(file, dotpos + 1, file);
+
+ // Break down file name into pieces
+ if (ExplodeString(file, "_", pieces, sizeof(pieces), sizeof(pieces[])) != sizeof(pieces))
+ {
+ continue;
+ }
+
+ // Extract info from the pieces
+ course = StringToInt(pieces[0]);
+ mode = GetModeIDFromString(pieces[1]);
+ style = GetStyleIDFromString(pieces[2]);
+ timeType = GetTimeTypeIDFromString(pieces[3]);
+ if (!GOKZ_IsValidCourse(course) || mode == -1 || style == -1 || timeType == -1)
+ {
+ continue;
+ }
+
+ // Add it to the cache
+ AddToReplayInfoCache(course, mode, style, timeType);
+ }
+
+ SortReplayInfoCache();
+
+ delete dir;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static int GetModeIDFromString(const char[] mode)
+{
+ for (int modeID = 0; modeID < MODE_COUNT; modeID++)
+ {
+ if (StrEqual(mode, gC_ModeNamesShort[modeID], false))
+ {
+ return modeID;
+ }
+ }
+ return -1;
+}
+
+static int GetStyleIDFromString(const char[] style)
+{
+ for (int styleID = 0; styleID < STYLE_COUNT; styleID++)
+ {
+ if (StrEqual(style, gC_StyleNamesShort[styleID], false))
+ {
+ return styleID;
+ }
+ }
+ return -1;
+}
+
+static int GetTimeTypeIDFromString(const char[] timeType)
+{
+ for (int timeTypeID = 0; timeTypeID < TIMETYPE_COUNT; timeTypeID++)
+ {
+ if (StrEqual(timeType, gC_TimeTypeNames[timeTypeID], false))
+ {
+ return timeTypeID;
+ }
+ }
+ return -1;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-replays/replay_menu.sp b/sourcemod/scripting/gokz-replays/replay_menu.sp
new file mode 100644
index 0000000..94acd66
--- /dev/null
+++ b/sourcemod/scripting/gokz-replays/replay_menu.sp
@@ -0,0 +1,139 @@
+/*
+ Lets player select a replay bot to play back.
+*/
+
+
+
+static int selectedReplayMode[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+void DisplayReplayModeMenu(int client)
+{
+ if (g_ReplayInfoCache.Length == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Replays Found (Map)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ Menu menu = new Menu(MenuHandler_ReplayMode);
+ menu.SetTitle("%T", "Replay Menu (Mode) - Title", client, gC_CurrentMap);
+ GOKZ_MenuAddModeItems(client, menu, false);
+ menu.Display(client, MENU_TIME_FOREVER);
+}
+
+
+
+// =====[ EVENTS ]=====
+
+public int MenuHandler_ReplayMode(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ selectedReplayMode[param1] = param2;
+ DisplayReplayMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+public int MenuHandler_Replay(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[4];
+ menu.GetItem(param2, info, sizeof(info));
+ int replayIndex = StringToInt(info);
+ int replayInfo[RP_CACHE_BLOCKSIZE];
+ g_ReplayInfoCache.GetArray(replayIndex, replayInfo);
+
+ char path[PLATFORM_MAX_PATH];
+ BuildPath(Path_SM, path, sizeof(path),
+ "%s/%s/%d_%s_%s_%s.%s",
+ RP_DIRECTORY_RUNS, gC_CurrentMap, replayInfo[0], gC_ModeNamesShort[replayInfo[1]], gC_StyleNamesShort[replayInfo[2]], gC_TimeTypeNames[replayInfo[3]], RP_FILE_EXTENSION);
+ if (!FileExists(path))
+ {
+ BuildPath(Path_SM, path, sizeof(path),
+ "%s/%d_%s_%s_%s.%s",
+ RP_DIRECTORY, gC_CurrentMap, replayInfo[0], gC_ModeNamesShort[replayInfo[1]], gC_StyleNamesShort[replayInfo[2]], gC_TimeTypeNames[replayInfo[3]], RP_FILE_EXTENSION);
+ if (!FileExists(path))
+ {
+ LogError("Failed to load file: \"%s\".", path);
+ GOKZ_PrintToChat(param1, true, "%t", "Replay Menu - No File");
+ return 0;
+ }
+ }
+
+ LoadReplayBot(param1, path);
+ }
+ else if (action == MenuAction_Cancel)
+ {
+ DisplayReplayModeMenu(param1);
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void DisplayReplayMenu(int client)
+{
+ Menu menu = new Menu(MenuHandler_Replay);
+ menu.SetTitle("%T", "Replay Menu - Title", client, gC_CurrentMap, gC_ModeNames[selectedReplayMode[client]]);
+ if (ReplayMenuAddItems(client, menu) > 0)
+ {
+ menu.Display(client, MENU_TIME_FOREVER);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Replays Found (Mode)", gC_ModeNames[selectedReplayMode[client]]);
+ GOKZ_PlayErrorSound(client);
+ DisplayReplayModeMenu(client);
+ }
+}
+
+// Returns the number of replay menu items added
+static int ReplayMenuAddItems(int client, Menu menu)
+{
+ int replaysAdded = 0;
+ int replayCount = g_ReplayInfoCache.Length;
+ int replayInfo[RP_CACHE_BLOCKSIZE];
+ char temp[32], indexString[4];
+
+ menu.RemoveAllItems();
+
+ for (int i = 0; i < replayCount; i++)
+ {
+ IntToString(i, indexString, sizeof(indexString));
+ g_ReplayInfoCache.GetArray(i, replayInfo);
+ if (replayInfo[1] != selectedReplayMode[client]) // Wrong mode!
+ {
+ continue;
+ }
+
+ if (replayInfo[0] == 0)
+ {
+ FormatEx(temp, sizeof(temp), "Main %s", gC_TimeTypeNames[replayInfo[3]]);
+ }
+ else
+ {
+ FormatEx(temp, sizeof(temp), "Bonus %d %s", replayInfo[0], gC_TimeTypeNames[replayInfo[3]]);
+ }
+ menu.AddItem(indexString, temp, ITEMDRAW_DEFAULT);
+
+ replaysAdded++;
+ }
+
+ return replaysAdded;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-saveloc.sp b/sourcemod/scripting/gokz-saveloc.sp
new file mode 100644
index 0000000..98d64c0
--- /dev/null
+++ b/sourcemod/scripting/gokz-saveloc.sp
@@ -0,0 +1,822 @@
+#include <sourcemod>
+
+#include <cstrike>
+#include <sdktools>
+
+#include <gokz/core>
+#include <gokz/kzplayer>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+#include <gokz/hud>
+#pragma newdecls required
+#pragma semicolon 1
+
+public Plugin myinfo =
+{
+ name = "GOKZ SaveLoc",
+ author = "JWL",
+ description = "Allows players to save/load locations that preserve position, angles, and velocity",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-saveloc.txt"
+#define LOADLOC_INVALIDATE_DURATION 0.12
+#define MAX_LOCATION_NAME_LENGTH 32
+
+enum struct Location {
+ // Location name must be first for FindString to work.
+ char locationName[MAX_LOCATION_NAME_LENGTH];
+ char locationCreator[MAX_NAME_LENGTH];
+
+ // GOKZ related states
+ int mode;
+ int course;
+ float currentTime;
+ ArrayList checkpointData;
+ int checkpointCount;
+ int teleportCount;
+ ArrayList undoTeleportData;
+
+ // Movement related states
+ int groundEnt;
+ int flags;
+ float position[3];
+ float angles[3];
+ float velocity[3];
+ float duckAmount;
+ bool ducking;
+ bool ducked;
+ float lastDuckTime;
+ float duckSpeed;
+ float stamina;
+ MoveType movetype;
+ float ladderNormal[3];
+ int collisionGroup;
+ float waterJumpTime;
+ bool hasWalkMovedSinceLastJump;
+ float ignoreLadderJumpTimeOffset;
+ float lastPositionAtFullCrouchSpeed[2];
+
+ void Create(int client, int target)
+ {
+ GetClientName(client, this.locationCreator, sizeof(Location::locationCreator));
+ this.groundEnt = GetEntPropEnt(target, Prop_Data, "m_hGroundEntity");
+ this.flags = GetEntityFlags(target);
+ this.mode = GOKZ_GetCoreOption(target, Option_Mode);
+ this.course = GOKZ_GetCourse(target);
+ GetClientAbsOrigin(target, this.position);
+ GetClientEyeAngles(target, this.angles);
+ GetEntPropVector(target, Prop_Data, "m_vecVelocity", this.velocity);
+ this.duckAmount = GetEntPropFloat(target, Prop_Send, "m_flDuckAmount");
+ this.ducking = !!GetEntProp(target, Prop_Send, "m_bDucking");
+ this.ducked = !!GetEntProp(target, Prop_Send, "m_bDucked");
+ this.lastDuckTime = GetEntPropFloat(target, Prop_Send, "m_flLastDuckTime");
+ this.duckSpeed = Movement_GetDuckSpeed(target);
+ this.stamina = GetEntPropFloat(target, Prop_Send, "m_flStamina");
+ this.movetype = Movement_GetMovetype(target);
+ GetEntPropVector(target, Prop_Send, "m_vecLadderNormal", this.ladderNormal);
+ this.collisionGroup = GetEntProp(target, Prop_Send, "m_CollisionGroup");
+ this.waterJumpTime = GetEntPropFloat(target, Prop_Data, "m_flWaterJumpTime");
+ this.hasWalkMovedSinceLastJump = !!GetEntProp(target, Prop_Data, "m_bHasWalkMovedSinceLastJump");
+ this.ignoreLadderJumpTimeOffset = GetEntPropFloat(target, Prop_Data, "m_ignoreLadderJumpTime") - GetGameTime();
+ GetLastPositionAtFullCrouchSpeed(target, this.lastPositionAtFullCrouchSpeed);
+
+ if (GOKZ_GetTimerRunning(target))
+ {
+ this.currentTime = GOKZ_GetTime(target);
+ }
+ else
+ {
+ this.currentTime = -1.0;
+ }
+ this.checkpointData = GOKZ_GetCheckpointData(target);
+ this.checkpointCount = GOKZ_GetCheckpointCount(target);
+ this.teleportCount = GOKZ_GetTeleportCount(target);
+ this.undoTeleportData = GOKZ_GetUndoTeleportData(target);
+ }
+
+ bool Load(int client)
+ {
+ // 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;
+ }
+ if (!GOKZ_SetMode(client, this.mode))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "LoadLoc - Mode Not Available");
+ }
+ GOKZ_SetCourse(client, this.course);
+ if (this.currentTime >= 0.0)
+ {
+ GOKZ_SetTime(client, this.currentTime);
+ }
+ GOKZ_SetCheckpointData(client, this.checkpointData, GOKZ_CHECKPOINT_VERSION);
+ GOKZ_SetCheckpointCount(client, this.checkpointCount);
+ GOKZ_SetTeleportCount(client, this.teleportCount);
+ GOKZ_SetUndoTeleportData(client, this.undoTeleportData, GOKZ_CHECKPOINT_VERSION);
+
+ SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", this.groundEnt);
+ SetEntityFlags(client, this.flags);
+ TeleportEntity(client, this.position, this.angles, this.velocity);
+ SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", this.duckAmount);
+ SetEntProp(client, Prop_Send, "m_bDucking", this.ducking);
+ SetEntProp(client, Prop_Send, "m_bDucked", this.ducked);
+ SetEntPropFloat(client, Prop_Send, "m_flLastDuckTime", this.lastDuckTime);
+ Movement_SetDuckSpeed(client, this.duckSpeed);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", this.stamina);
+ Movement_SetMovetype(client, this.movetype);
+ SetEntPropVector(client, Prop_Send, "m_vecLadderNormal", this.ladderNormal);
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", this.collisionGroup);
+ SetEntPropFloat(client, Prop_Data, "m_flWaterJumpTime", this.waterJumpTime);
+ SetEntProp(client, Prop_Data, "m_bHasWalkMovedSinceLastJump", this.hasWalkMovedSinceLastJump);
+ SetEntPropFloat(client, Prop_Data, "m_ignoreLadderJumpTime", this.ignoreLadderJumpTimeOffset + GetGameTime());
+ SetLastPositionAtFullCrouchSpeed(client, this.lastPositionAtFullCrouchSpeed);
+
+ GOKZ_InvalidateRun(client);
+ return true;
+ }
+}
+
+ArrayList gA_Locations;
+bool gB_LocMenuOpen[MAXPLAYERS + 1];
+bool gB_UsedLoc[MAXPLAYERS + 1];
+int gI_MostRecentLocation[MAXPLAYERS + 1];
+float gF_LastLoadlocTime[MAXPLAYERS + 1];
+
+bool gB_GOKZHUD;
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-saveloc");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-saveloc.phrases");
+
+ HookEvents();
+ RegisterCommands();
+ CreateArrays();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZHUD = LibraryExists("gokz-hud");
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+ gB_GOKZHUD = gB_GOKZHUD || StrEqual(name, "gokz-hud");
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ gB_GOKZHUD = gB_GOKZHUD && !StrEqual(name, "gokz-hud");
+}
+
+public void OnMapStart()
+{
+ ClearLocations();
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void OnClientPutInServer(int client)
+{
+ gF_LastLoadlocTime[client] = 0.0;
+}
+
+public void OnPlayerDeath(Event event, const char[] name, bool dontBroadcast)
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+
+ CloseLocMenu(client);
+}
+
+public void OnPlayerJoinTeam(Event event, const char[] name, bool dontBroadcast)
+{
+ int client = GetClientOfUserId(event.GetInt("userid"));
+ int team = event.GetInt("team");
+
+ if (team == CS_TEAM_SPECTATOR)
+ {
+ CloseLocMenu(client);
+ }
+}
+
+public Action GOKZ_OnTimerStart(int client, int course)
+{
+ CloseLocMenu(client);
+ gB_UsedLoc[client] = false;
+ if (GetGameTime() < gF_LastLoadlocTime[client] + LOADLOC_INVALIDATE_DURATION)
+ {
+ return Plugin_Stop;
+ }
+ return Plugin_Continue;
+}
+
+public Action GOKZ_OnTimerEnd(int client, int course, float time)
+{
+ if (gB_UsedLoc[client])
+ {
+ PrintEndTimeString_SaveLoc(client, course, time);
+ }
+ return Plugin_Continue;
+}
+
+// =====[ GENERAL ]=====
+
+void HookEvents()
+{
+ HookEvent("player_death", OnPlayerDeath);
+ HookEvent("player_team", OnPlayerJoinTeam);
+}
+
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_saveloc", Command_SaveLoc, "[KZ] Save location. Usage: !saveloc <name>");
+ RegConsoleCmd("sm_loadloc", Command_LoadLoc, "[KZ] Load location. Usage: !loadloc <#id OR name>");
+ RegConsoleCmd("sm_prevloc", Command_PrevLoc, "[KZ] Go back to the previous location.");
+ RegConsoleCmd("sm_nextloc", Command_NextLoc, "[KZ] Go forward to the next location.");
+ RegConsoleCmd("sm_locmenu", Command_LocMenu, "[KZ] Open location menu.");
+ RegConsoleCmd("sm_nameloc", Command_NameLoc, "[KZ] Name location. Usage: !nameloc <#id> <name>");
+}
+
+public Action Command_SaveLoc(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ int target = -1;
+ if (!IsPlayerAlive(client))
+ {
+ KZPlayer player = KZPlayer(client);
+ target = player.ObserverTarget;
+ if (target == -1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Must Be Alive");
+ GOKZ_PlayErrorSound(client);
+ return Plugin_Handled;
+ }
+ }
+
+ if (args == 0)
+ {
+ // save location with empty <name>
+ SaveLocation(client, "", target);
+ }
+ else if (args == 1)
+ {
+ // get location <name>
+ char arg[MAX_LOCATION_NAME_LENGTH];
+ GetCmdArg(1, arg, sizeof(arg));
+
+ if (IsValidLocationName(arg))
+ {
+ // save location with <name>
+ SaveLocation(client, arg, target);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Naming Format");
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "SaveLoc - Usage");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action Command_LoadLoc(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ else if (!IsPlayerAlive(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Must Be Alive");
+ return Plugin_Handled;
+ }
+ else if (gA_Locations.Length == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Locations Found");
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ {
+ // load most recent location
+ int id = gI_MostRecentLocation[client];
+ LoadLocation(client, id);
+ }
+ else if (args == 1)
+ {
+ // get location <#id OR name>
+ char arg[MAX_LOCATION_NAME_LENGTH];
+ GetCmdArg(1, arg, sizeof(arg));
+ int id;
+
+ if (arg[0] == '#')
+ {
+ // load location <#id>
+ id = StringToInt(arg[1]);
+ }
+ else
+ {
+ // load location <name>
+ id = gA_Locations.FindString(arg);
+ }
+
+ if (IsValidLocationId(id))
+ {
+ if (LoadLocation(client, id))
+ {
+ gF_LastLoadlocTime[client] = GetGameTime();
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Location Not Found");
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "LoadLoc - Usage");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action Command_PrevLoc(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ else if (gA_Locations.Length == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Locations Found");
+ return Plugin_Handled;
+ }
+ else if (gI_MostRecentLocation[client] <= 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "PrevLoc - Can't Prev Location (No Location Found)");
+ return Plugin_Handled;
+ }
+ LoadLocation(client, gI_MostRecentLocation[client] - 1);
+ return Plugin_Handled;
+}
+
+public Action Command_NextLoc(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ else if (gA_Locations.Length == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Locations Found");
+ return Plugin_Handled;
+ }
+ else if (gI_MostRecentLocation[client] >= gA_Locations.Length - 1)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NextLoc - Can't Next Location (No Location Found)");
+ return Plugin_Handled;
+ }
+ LoadLocation(client, gI_MostRecentLocation[client] + 1);
+ return Plugin_Handled;
+}
+
+public Action Command_NameLoc(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ else if (gA_Locations.Length == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Locations Found");
+ return Plugin_Handled;
+ }
+
+ if (args == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Usage");
+ }
+ else if (args == 1)
+ {
+ // name most recent location
+ char arg[MAX_LOCATION_NAME_LENGTH];
+ GetCmdArg(1, arg, sizeof(arg));
+ int id = gI_MostRecentLocation[client];
+
+ if (IsValidLocationName(arg) && IsClientLocationCreator(client, id))
+ {
+ NameLocation(client, id, arg);
+ }
+ else if (!IsClientLocationCreator(client, id))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Not Creator");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Naming Format");
+ }
+ }
+ else if (args == 2)
+ {
+ // name specified location
+ char arg1[MAX_LOCATION_NAME_LENGTH];
+ char arg2[MAX_LOCATION_NAME_LENGTH];
+ GetCmdArg(1, arg1, sizeof(arg1));
+ GetCmdArg(2, arg2, sizeof(arg2));
+ int id = StringToInt(arg1[1]);
+
+ if (IsValidLocationId(id))
+ {
+
+ if (IsValidLocationName(arg2) && IsClientLocationCreator(client, id))
+ {
+ NameLocation(client, id, arg2);
+ }
+ else if (!IsClientLocationCreator(client, id))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Not Creator");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Naming Format");
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Location Not Found");
+ }
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - Usage");
+ }
+
+ return Plugin_Handled;
+}
+
+public Action Command_LocMenu(int client, int args)
+{
+ if (!IsValidClient(client))
+ {
+ return Plugin_Handled;
+ }
+ else if (!IsPlayerAlive(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Must Be Alive");
+ return Plugin_Handled;
+ }
+ else if (gA_Locations.Length == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "No Locations Found");
+ return Plugin_Handled;
+ }
+
+ ShowLocMenu(client);
+
+ return Plugin_Handled;
+}
+
+// ====[ SAVELOC MENU ]====
+
+void ShowLocMenu(int client)
+{
+ Menu locMenu = new Menu(LocMenuHandler, MENU_ACTIONS_ALL);
+ locMenu.SetTitle("%t", "LocMenu - Title");
+
+ // fill the menu with all locations
+ for (int i = 0; i < gA_Locations.Length; i++)
+ {
+ char item[MAX_LOCATION_NAME_LENGTH];
+ Format(item, sizeof(item), "%i", i);
+ locMenu.AddItem(item, item);
+ }
+
+ // calculate which page of the menu contains client's most recent location
+ int firstItem;
+ if (gI_MostRecentLocation[client] > 5)
+ {
+ firstItem = gI_MostRecentLocation[client] - (gI_MostRecentLocation[client] % 6);
+ }
+
+ locMenu.DisplayAt(client, firstItem, MENU_TIME_FOREVER);
+}
+
+
+
+// ====[ SAVELOC MENU HANDLER ]====
+
+public int LocMenuHandler(Menu menu, MenuAction action, int client, int choice)
+{
+ switch (action)
+ {
+ case MenuAction_Display:
+ {
+ gB_LocMenuOpen[client] = true;
+ }
+
+ case MenuAction_DisplayItem:
+ {
+ Location loc;
+ char item[MAX_LOCATION_NAME_LENGTH];
+ menu.GetItem(choice, item, sizeof(item));
+
+ int id = StringToInt(item);
+ gA_Locations.GetArray(id, loc);
+ char name[MAX_LOCATION_NAME_LENGTH];
+ strcopy(name, sizeof(name), loc.locationName);
+
+ if (id == gI_MostRecentLocation[client])
+ {
+ Format(item, sizeof(item), "> #%i %s", id, name);
+ }
+ else
+ {
+ Format(item, sizeof(item), "#%i %s", id, name);
+ }
+
+ return RedrawMenuItem(item);
+ }
+
+ case MenuAction_Select:
+ {
+ char item[MAX_LOCATION_NAME_LENGTH];
+ menu.GetItem(choice, item, sizeof(item));
+ ReplaceString(item, sizeof(item), "#", "");
+ int id = StringToInt(item);
+
+ LoadLocation(client, id);
+ }
+
+ case MenuAction_Cancel:
+ {
+ gB_LocMenuOpen[client] = false;
+ }
+
+ case MenuAction_End:
+ {
+ delete menu;
+ }
+ }
+
+ return 0;
+}
+
+
+
+// ====[ SAVE LOCATION ]====
+
+void SaveLocation(int client, char[] name, int target)
+{
+ Location loc;
+ if (target == -1)
+ {
+ target = client;
+ }
+ loc.Create(client, target);
+ strcopy(loc.locationName, sizeof(Location::locationName), name);
+ GetClientName(client, loc.locationCreator, sizeof(loc.locationCreator));
+ gA_Locations.PushArray(loc);
+ gI_MostRecentLocation[client] = gA_Locations.Length - 1;
+
+ GOKZ_PrintToChat(client, true, "%t", "SaveLoc - ID Name", gA_Locations.Length - 1, name);
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ RefreshLocMenu(i);
+ }
+}
+
+
+
+// ====[ LOAD LOCATION ]====
+
+bool LoadLocation(int client, int id)
+{
+ if (!IsPlayerAlive(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Must Be Alive");
+ return false;
+ }
+ char clientName[MAX_NAME_LENGTH];
+
+ GetClientName(client, clientName, sizeof(clientName));
+ Location loc;
+ gA_Locations.GetArray(id, loc);
+ if (loc.Load(client))
+ {
+ gB_UsedLoc[client] = true;
+ if (gB_GOKZHUD)
+ {
+ GOKZ_HUD_ForceUpdateTPMenu(client);
+ }
+ }
+ else
+ {
+ return false;
+ }
+ // print message if loading new location
+ if (gI_MostRecentLocation[client] != id)
+ {
+ gI_MostRecentLocation[client] = id;
+
+ if (StrEqual(clientName, loc.locationCreator))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "LoadLoc - ID Name", id, loc.locationName);
+ }
+ else
+ {
+ if (StrEqual(loc.locationName, ""))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "LoadLoc - ID Creator", id, loc.locationCreator);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "LoadLoc - ID Name Creator", id, loc.locationName, loc.locationCreator);
+ }
+ }
+ }
+
+ RefreshLocMenu(client);
+
+ return true;
+}
+
+
+
+// ====[ NAME LOCATION ]====
+
+void NameLocation(int client, int id, char[] name)
+{
+ Location loc;
+ gA_Locations.GetArray(id, loc);
+ strcopy(loc.locationName, sizeof(Location::locationName), name);
+
+ GOKZ_PrintToChat(client, true, "%t", "NameLoc - ID Name", id, name);
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ RefreshLocMenu(i);
+ }
+}
+
+
+
+// =====[ HELPER FUNCTIONS ]=====
+
+void CreateArrays()
+{
+ gA_Locations = new ArrayList(sizeof(Location));
+}
+
+void ClearLocations()
+{
+ Location loc;
+ for (int i = 0; i < gA_Locations.Length; i++)
+ {
+ // Prevent memory leak
+ gA_Locations.GetArray(i, loc);
+ delete loc.checkpointData;
+ }
+ gA_Locations.Clear();
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ gI_MostRecentLocation[i] = -1;
+ gB_LocMenuOpen[i] = false;
+ }
+}
+
+void RefreshLocMenu(int client)
+{
+ if (gB_LocMenuOpen[client])
+ {
+ ShowLocMenu(client);
+ }
+}
+
+void CloseLocMenu(int client)
+{
+ if (gB_LocMenuOpen[client])
+ {
+ CancelClientMenu(client, true);
+ gB_LocMenuOpen[client] = false;
+ }
+}
+
+bool IsValidLocationId(int id)
+{
+ return !(id < 0) && !(id > gA_Locations.Length - 1);
+}
+
+bool IsValidLocationName(char[] name)
+{
+ // check if location name starts with letter and is unique
+ return IsCharAlpha(name[0]) && gA_Locations.FindString(name) == -1;
+}
+
+bool IsClientLocationCreator(int client, int id)
+{
+ char clientName[MAX_NAME_LENGTH];
+ Location loc;
+ gA_Locations.GetArray(id, loc);
+ GetClientName(client, clientName, sizeof(clientName));
+
+ return StrEqual(clientName, loc.locationCreator);
+}
+
+void GetLastPositionAtFullCrouchSpeed(int client, float origin[2])
+{
+ // m_vecLastPositionAtFullCrouchSpeed is right after m_flDuckSpeed.
+ int baseOffset = FindSendPropInfo("CBasePlayer", "m_flDuckSpeed");
+ origin[0] = GetEntDataFloat(client, baseOffset + 4);
+ origin[1] = GetEntDataFloat(client, baseOffset + 8);
+}
+
+void SetLastPositionAtFullCrouchSpeed(int client, float origin[2])
+{
+ int baseOffset = FindSendPropInfo("CBasePlayer", "m_flDuckSpeed");
+ SetEntDataFloat(client, baseOffset + 4, origin[0]);
+ SetEntDataFloat(client, baseOffset + 8, origin[1]);
+}
+
+// ====[ PRIVATE ]====
+
+static void PrintEndTimeString_SaveLoc(int client, int course, float time)
+{
+ if (course == 0)
+ {
+ switch (GOKZ_GetTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Beat Map (NUB)",
+ client,
+ GOKZ_FormatTime(time),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Beat Map (PRO)",
+ client,
+ GOKZ_FormatTime(time),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+ else
+ {
+ switch (GOKZ_GetTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Beat Bonus (NUB)",
+ client,
+ GOKZ_GetCourse(client),
+ GOKZ_FormatTime(time),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Beat Bonus (PRO)",
+ client,
+ GOKZ_GetCourse(client),
+ GOKZ_FormatTime(time),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+}
diff --git a/sourcemod/scripting/gokz-slayonend.sp b/sourcemod/scripting/gokz-slayonend.sp
new file mode 100644
index 0000000..c83c4e6
--- /dev/null
+++ b/sourcemod/scripting/gokz-slayonend.sp
@@ -0,0 +1,190 @@
+#include <sourcemod>
+
+#include <gokz/core>
+#include <gokz/slayonend>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Slay On End",
+ author = "DanZay",
+ description = "Adds option to slay the player upon ending their timer",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-slayonend.txt"
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatGeneral;
+TopMenuObject gTMO_ItemSlayOnEnd;
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-slayonend");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-slayonend.phrases");
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed)
+{
+ OnTimerEnd_SlayOnEnd(client);
+}
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ OnOptionChanged_Options(client, option, newValue);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+
+
+// =====[ SLAY ON END ]=====
+
+void OnTimerEnd_SlayOnEnd(int client)
+{
+ if (GOKZ_GetOption(client, SLAYONEND_OPTION_NAME) == SlayOnEnd_Enabled)
+ {
+ CreateTimer(3.0, Timer_SlayPlayer, GetClientUserId(client));
+ }
+}
+
+public Action Timer_SlayPlayer(Handle timer, int userid)
+{
+ int client = GetClientOfUserId(userid);
+ if (IsValidClient(client))
+ {
+ ForcePlayerSuicide(client);
+ }
+ return Plugin_Continue;
+}
+
+
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOption();
+}
+
+void RegisterOption()
+{
+ GOKZ_RegisterOption(SLAYONEND_OPTION_NAME, SLAYONEND_OPTION_DESCRIPTION,
+ OptionType_Int, SlayOnEnd_Disabled, 0, SLAYONEND_COUNT - 1);
+}
+
+void OnOptionChanged_Options(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, SLAYONEND_OPTION_NAME))
+ {
+ switch (newValue)
+ {
+ case SlayOnEnd_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Slay On End - Disable");
+ }
+ case SlayOnEnd_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Slay On End - Enable");
+ }
+ }
+ }
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+ gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY);
+ gTMO_ItemSlayOnEnd = gTM_Options.AddItem(SLAYONEND_OPTION_NAME, TopMenuHandler_SlayOnEnd, gTMO_CatGeneral);
+}
+
+public void TopMenuHandler_SlayOnEnd(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (topobj_id != gTMO_ItemSlayOnEnd)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ if (GOKZ_GetOption(param, SLAYONEND_OPTION_NAME) == SlayOnEnd_Disabled)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - Slay On End", param,
+ "Options Menu - Disabled", param);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - Slay On End", param,
+ "Options Menu - Enabled", param);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_CycleOption(param, SLAYONEND_OPTION_NAME);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-spec.sp b/sourcemod/scripting/gokz-spec.sp
new file mode 100644
index 0000000..29f842f
--- /dev/null
+++ b/sourcemod/scripting/gokz-spec.sp
@@ -0,0 +1,323 @@
+#include <sourcemod>
+
+#include <cstrike>
+
+#include <gokz/core>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Spectate Menu",
+ author = "DanZay",
+ description = "Provides easy ways to spectate players",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-spec.txt"
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-spec");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("common.phrases");
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-spec.phrases");
+
+ RegisterCommands();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+}
+
+
+
+// =====[ SPEC MENU ]=====
+
+int DisplaySpecMenu(int client, bool useFilter = false, char[] filter = "")
+{
+ Menu menu = new Menu(MenuHandler_Spec);
+ menu.SetTitle("%T", "Spec Menu - Title", client);
+ int menuItems = SpecMenuAddItems(client, menu, useFilter, filter);
+ if (menuItems == 0 || menuItems == 1)
+ {
+ delete menu;
+ }
+ else
+ {
+ menu.Display(client, MENU_TIME_FOREVER);
+ }
+ return menuItems;
+}
+
+bool Spectate(int client)
+{
+ if (!CanSpectate(client))
+ {
+ return false;
+ }
+
+ GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR);
+
+ // Put player in free look mode and apply according movetype
+ SetEntProp(client, Prop_Send, "m_iObserverMode", 6);
+ SetEntityMoveType(client, MOVETYPE_OBSERVER);
+ return true;
+}
+
+// Returns whether change to spectating the target was successful
+bool SpectatePlayer(int client, int target, bool printMessage = true)
+{
+ if (!CanSpectate(client))
+ {
+ return false;
+ }
+
+ if (target == client)
+ {
+ Spectate(client);
+ return true;
+ }
+ else if (!IsPlayerAlive(target))
+ {
+ if (printMessage)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Spectate Failure (Dead)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+
+ GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR);
+ SetEntProp(client, Prop_Send, "m_iObserverMode", 4);
+ SetEntPropEnt(client, Prop_Send, "m_hObserverTarget", target);
+
+ return true;
+}
+
+bool CanSpectate(int client)
+{
+ return !IsPlayerAlive(client) || GOKZ_GetPaused(client) || GOKZ_GetCanPause(client);
+}
+
+public int MenuHandler_Spec(Menu menu, MenuAction action, int param1, int param2)
+{
+ if (action == MenuAction_Select)
+ {
+ char info[16];
+ menu.GetItem(param2, info, sizeof(info));
+ int target = GetClientOfUserId(StringToInt(info));
+
+ if (!IsValidClient(target))
+ {
+ GOKZ_PrintToChat(param1, true, "%t", "Player No Longer Valid");
+ GOKZ_PlayErrorSound(param1);
+ DisplaySpecMenu(param1);
+ }
+ else if (!SpectatePlayer(param1, target))
+ {
+ DisplaySpecMenu(param1);
+ }
+ }
+ else if (action == MenuAction_End)
+ {
+ delete menu;
+ }
+ return 0;
+}
+
+// Returns number of items added to the menu
+int SpecMenuAddItems(int client, Menu menu, bool useFilter, char[] filter)
+{
+ char display[MAX_NAME_LENGTH + 4];
+ int targetCount = 0;
+ int latestResult;
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (!IsClientInGame(i) || !IsPlayerAlive(i) || i == client)
+ {
+ continue;
+ }
+ if (useFilter)
+ {
+ FormatEx(display, sizeof(display), "%N", i);
+ if (StrContains(display, filter, false) != -1)
+ {
+ if (IsFakeClient(i))
+ {
+ FormatEx(display, sizeof(display), "BOT %N", i);
+ }
+ }
+ else // If it doesn't fit the filter, move on
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if (IsFakeClient(i))
+ {
+ FormatEx(display, sizeof(display), "BOT %N", i);
+ }
+ else
+ {
+ FormatEx(display, sizeof(display), "%N", i);
+ }
+ }
+ latestResult = i;
+ menu.AddItem(IntToStringEx(GetClientUserId(i)), display, ITEMDRAW_DEFAULT);
+ targetCount++;
+ }
+ // The only spectate-able player is the latest result, this happens when the player issuing the command also fits in the filter
+ if (targetCount == 1)
+ {
+ SpectatePlayer(client, latestResult);
+ }
+
+ return targetCount;
+}
+
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_spec", CommandSpec, "[KZ] Spectate another player. Usage: !spec <player>");
+ RegConsoleCmd("sm_specs", CommandSpecs, "[KZ] List currently spectating players in chat.");
+ RegConsoleCmd("sm_speclist", CommandSpecs, "[KZ] List currently spectating players in chat.");
+}
+
+public Action CommandSpec(int client, int args)
+{
+ // If no arguments, display the spec menu
+ if (args < 1)
+ {
+ if (DisplaySpecMenu(client) == 0)
+ {
+ // No targets, so just join spec
+ Spectate(client);
+ }
+ }
+ // Otherwise try to spectate the player
+ else
+ {
+ char specifiedPlayer[MAX_NAME_LENGTH];
+ GetCmdArg(1, specifiedPlayer, sizeof(specifiedPlayer));
+
+ char targetName[MAX_TARGET_LENGTH];
+ int targetList[1], targetCount;
+ bool tnIsML;
+ int flags = COMMAND_FILTER_NO_MULTI | COMMAND_FILTER_NO_IMMUNITY | COMMAND_FILTER_ALIVE;
+
+ if ((targetCount = ProcessTargetString(
+ specifiedPlayer,
+ client,
+ targetList,
+ 1,
+ flags,
+ targetName,
+ sizeof(targetName),
+ tnIsML)) == 1)
+ {
+ SpectatePlayer(client, targetList[0]);
+ }
+ else if (targetCount == COMMAND_TARGET_AMBIGUOUS)
+ {
+ DisplaySpecMenu(client, true, specifiedPlayer);
+ }
+ else
+ {
+ ReplyToTargetError(client, targetCount);
+ }
+
+ }
+ return Plugin_Handled;
+}
+
+public Action CommandSpecs(int client, int args)
+{
+ int specs = 0;
+ char specNames[1024];
+
+ int target = IsPlayerAlive(client) ? client : GetObserverTarget(client);
+ int targetSpecs = 0;
+ char targetSpecNames[1024];
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (IsClientInGame(i) && !IsFakeClient(i) && IsSpectating(i))
+ {
+ specs++;
+ if (specs == 1)
+ {
+ FormatEx(specNames, sizeof(specNames), "{lime}%N", i);
+ }
+ else
+ {
+ Format(specNames, sizeof(specNames), "%s{grey}, {lime}%N", specNames, i);
+ }
+
+ if (target != -1 && GetObserverTarget(i) == target)
+ {
+ targetSpecs++;
+ if (targetSpecs == 1)
+ {
+ FormatEx(targetSpecNames, sizeof(targetSpecNames), "{lime}%N", i);
+ }
+ else
+ {
+ Format(targetSpecNames, sizeof(targetSpecNames), "%s{grey}, {lime}%N", targetSpecNames, i);
+ }
+ }
+ }
+ }
+
+ if (specs == 0)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Spectator List (None)");
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Spectator List", specs, specNames);
+ if (targetSpecs == 0)
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Target Spectator List (None)", target);
+ }
+ else
+ {
+ GOKZ_PrintToChat(client, false, "%t", "Target Spectator List", target, targetSpecs, targetSpecNames);
+ }
+ }
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-tips.sp b/sourcemod/scripting/gokz-tips.sp
new file mode 100644
index 0000000..8a84b9e
--- /dev/null
+++ b/sourcemod/scripting/gokz-tips.sp
@@ -0,0 +1,357 @@
+#include <sourcemod>
+
+#include <gokz/core>
+#include <gokz/tips>
+
+#include <autoexecconfig>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#include <gokz/kzplayer>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Tips",
+ author = "DanZay",
+ description = "Prints tips to chat periodically based on loaded plugins",
+ version = GOKZ_VERSION,
+ url = GOKZ_SOURCE_URL
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-tips.txt"
+
+bool gC_PluginsWithTipsLoaded[TIPS_PLUGINS_COUNT];
+ArrayList g_TipPhrases;
+int gI_CurrentTip;
+Handle gH_TipTimer;
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatGeneral;
+TopMenuObject gTMO_ItemTips;
+ConVar gCV_gokz_tips_interval;
+
+
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-tips");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-tips.phrases");
+ LoadTranslations("gokz-tips-tips.phrases");
+ LoadTranslations("gokz-tips-core.phrases");
+
+ // Load translations of tips for other GOKZ plugins
+ char translation[PLATFORM_MAX_PATH];
+ for (int i = 0; i < TIPS_PLUGINS_COUNT; i++)
+ {
+ FormatEx(translation, sizeof(translation), "gokz-tips-%s.phrases", gC_PluginsWithTips[i]);
+ LoadTranslations(translation);
+ }
+
+ CreateConVars();
+ RegisterCommands();
+ CreateTipsTimer();
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ char gokzPlugin[PLATFORM_MAX_PATH];
+ for (int i = 0; i < TIPS_PLUGINS_COUNT; i++)
+ {
+ FormatEx(gokzPlugin, sizeof(gokzPlugin), "gokz-%s", gC_PluginsWithTips[i]);
+ gC_PluginsWithTipsLoaded[i] = LibraryExists(gokzPlugin);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ char gokzPlugin[PLATFORM_MAX_PATH];
+ for (int i = 0; i < TIPS_PLUGINS_COUNT; i++)
+ {
+ FormatEx(gokzPlugin, sizeof(gokzPlugin), "gokz-%s", gC_PluginsWithTips[i]);
+ gC_PluginsWithTipsLoaded[i] = gC_PluginsWithTipsLoaded[i] || StrEqual(name, gokzPlugin);
+ }
+}
+
+public void OnLibraryRemoved(const char[] name)
+{
+ char gokzPlugin[PLATFORM_MAX_PATH];
+ for (int i = 0; i < TIPS_PLUGINS_COUNT; i++)
+ {
+ FormatEx(gokzPlugin, sizeof(gokzPlugin), "gokz-%s", gC_PluginsWithTips[i]);
+ gC_PluginsWithTipsLoaded[i] = gC_PluginsWithTipsLoaded[i] && !StrEqual(name, gokzPlugin);
+ }
+}
+
+
+
+// =====[ CLIENT EVENTS ]=====
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ OnOptionChanged_Options(client, option, newValue);
+}
+
+
+
+// =====[ OTHER EVENTS ]=====
+
+public void OnMapStart()
+{
+ LoadTipPhrases();
+}
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+
+
+// =====[ CONVARS ]=====
+
+void CreateConVars()
+{
+ AutoExecConfig_SetFile("gokz-tips", "sourcemod/gokz");
+ AutoExecConfig_SetCreateFile(true);
+
+ gCV_gokz_tips_interval = AutoExecConfig_CreateConVar("gokz_tips_interval", "75", "How often GOKZ tips are printed to chat in seconds.", _, true, 1.0, false);
+ gCV_gokz_tips_interval.AddChangeHook(OnConVarChanged);
+
+ AutoExecConfig_ExecuteFile();
+ AutoExecConfig_CleanFile();
+}
+
+public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ if (convar == gCV_gokz_tips_interval)
+ {
+ CreateTipsTimer();
+ }
+}
+
+
+
+// =====[ TIPS ]=====
+
+void LoadTipPhrases()
+{
+ if (g_TipPhrases == null)
+ {
+ g_TipPhrases = new ArrayList(64, 0);
+ }
+ else
+ {
+ g_TipPhrases.Clear();
+ }
+
+ char tipsPath[PLATFORM_MAX_PATH];
+
+ BuildPath(Path_SM, tipsPath, sizeof(tipsPath), "translations/%s", TIPS_TIPS);
+ LoadTipPhrasesFromFile(tipsPath);
+
+ BuildPath(Path_SM, tipsPath, sizeof(tipsPath), "translations/%s", TIPS_CORE);
+ LoadTipPhrasesFromFile(tipsPath);
+
+ // Load tips for other loaded GOKZ plugins
+ for (int i = 0; i < TIPS_PLUGINS_COUNT; i++)
+ {
+ if (gC_PluginsWithTipsLoaded[i])
+ {
+ BuildPath(Path_SM, tipsPath, sizeof(tipsPath), "translations/gokz-tips-%s.phrases.txt", gC_PluginsWithTips[i]);
+ LoadTipPhrasesFromFile(tipsPath);
+ }
+ }
+
+ ShuffleTipPhrases();
+}
+
+void LoadTipPhrasesFromFile(const char[] filePath)
+{
+ KeyValues kv = new KeyValues("Phrases");
+ if (!kv.ImportFromFile(filePath))
+ {
+ SetFailState("Failed to load file: \"%s\".", filePath);
+ }
+
+ char phraseName[64];
+ kv.GotoFirstSubKey(true);
+ do
+ {
+ kv.GetSectionName(phraseName, sizeof(phraseName));
+ g_TipPhrases.PushString(phraseName);
+ } while (kv.GotoNextKey(true));
+
+ delete kv;
+}
+
+void ShuffleTipPhrases()
+{
+ for (int i = g_TipPhrases.Length - 1; i >= 1; i--)
+ {
+ int j = GetRandomInt(0, i);
+ char tempStringI[64];
+ g_TipPhrases.GetString(i, tempStringI, sizeof(tempStringI));
+ char tempStringJ[64];
+ g_TipPhrases.GetString(j, tempStringJ, sizeof(tempStringJ));
+ g_TipPhrases.SetString(i, tempStringJ);
+ g_TipPhrases.SetString(j, tempStringI);
+ }
+}
+
+void CreateTipsTimer()
+{
+ if (gH_TipTimer != null)
+ {
+ delete gH_TipTimer;
+ }
+ gH_TipTimer = CreateTimer(gCV_gokz_tips_interval.FloatValue, Timer_PrintTip, _, TIMER_REPEAT);
+}
+
+public Action Timer_PrintTip(Handle timer)
+{
+ char tip[256];
+ g_TipPhrases.GetString(gI_CurrentTip, tip, sizeof(tip));
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ KZPlayer player = KZPlayer(client);
+ if (player.InGame && player.Tips != Tips_Disabled)
+ {
+ GOKZ_PrintToChat(client, true, "%t", tip);
+ }
+ }
+
+ gI_CurrentTip = NextIndex(gI_CurrentTip, g_TipPhrases.Length);
+ return Plugin_Continue;
+}
+
+
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOption();
+}
+
+void RegisterOption()
+{
+ GOKZ_RegisterOption(TIPS_OPTION_NAME, TIPS_OPTION_DESCRIPTION,
+ OptionType_Int, Tips_Enabled, 0, TIPS_COUNT - 1);
+}
+
+void OnOptionChanged_Options(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, TIPS_OPTION_NAME))
+ {
+ switch (newValue)
+ {
+ case Tips_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Tips - Disable");
+ }
+ case Tips_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - Tips - Enable");
+ }
+ }
+ }
+}
+
+
+
+// =====[ OPTIONS MENU ]=====
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+ gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY);
+ gTMO_ItemTips = gTM_Options.AddItem(TIPS_OPTION_NAME, TopMenuHandler_Tips, gTMO_CatGeneral);
+}
+
+public void TopMenuHandler_Tips(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (topobj_id != gTMO_ItemTips)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ if (GOKZ_GetOption(param, TIPS_OPTION_NAME) == Tips_Disabled)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - Tips", param,
+ "Options Menu - Disabled", param);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - Tips", param,
+ "Options Menu - Enabled", param);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_CycleOption(param, TIPS_OPTION_NAME);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
+
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_tips", CommandToggleTips, "[KZ] Toggle seeing help and tips.");
+}
+
+public Action CommandToggleTips(int client, int args)
+{
+ if (GOKZ_GetOption(client, TIPS_OPTION_NAME) == Tips_Disabled)
+ {
+ GOKZ_SetOption(client, TIPS_OPTION_NAME, Tips_Enabled);
+ }
+ else
+ {
+ GOKZ_SetOption(client, TIPS_OPTION_NAME, Tips_Disabled);
+ }
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-tpanglefix.sp b/sourcemod/scripting/gokz-tpanglefix.sp
new file mode 100644
index 0000000..6297795
--- /dev/null
+++ b/sourcemod/scripting/gokz-tpanglefix.sp
@@ -0,0 +1,277 @@
+#include <sourcemod>
+
+#include <dhooks>
+
+#include <gokz/core>
+#include <gokz/tpanglefix>
+
+#include <autoexecconfig>
+
+#undef REQUIRE_EXTENSIONS
+#undef REQUIRE_PLUGIN
+#include <updater>
+
+#pragma newdecls required
+#pragma semicolon 1
+
+
+
+public Plugin myinfo =
+{
+ name = "GOKZ Teleport Angle Fix",
+ author = "zer0.k",
+ description = "Fix teleporting not modifying player's view angles due to packet loss",
+ version = GOKZ_VERSION,
+ url = "https://github.com/KZGlobalTeam/gokz"
+};
+
+#define UPDATER_URL GOKZ_UPDATER_BASE_URL..."gokz-tpanglefix.txt"
+
+TopMenu gTM_Options;
+TopMenuObject gTMO_CatGeneral;
+TopMenuObject gTMO_ItemTPAngleFix;
+Address gA_ViewAnglePatchAddress;
+bool gB_EnableFix[MAXPLAYERS + 1];
+DynamicDetour gH_WriteViewAngleUpdate;
+int gI_ClientOffset;
+
+// =====[ PLUGIN EVENTS ]=====
+
+public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
+{
+ RegPluginLibrary("gokz-tpanglefix");
+ return APLRes_Success;
+}
+
+public void OnPluginStart()
+{
+ LoadTranslations("gokz-common.phrases");
+ LoadTranslations("gokz-tpanglefix.phrases");
+
+ SetupPatch();
+ HookEvents();
+ RegisterCommands();
+}
+
+void SetupPatch()
+{
+ GameData gamedataConf = LoadGameConfigFile("gokz-tpanglefix.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-tpanglefix gamedata");
+ }
+ // Get the patching address
+ Address addr = GameConfGetAddress(gamedataConf, "WriteViewAngleUpdate");
+ if(addr == Address_Null)
+ {
+ SetFailState("Can't find WriteViewAngleUpdate address.");
+ }
+
+ // Get the offset from the start of the signature to the start of our patch area.
+ int offset = GameConfGetOffset(gamedataConf, "WriteViewAngleUpdateReliableOffset");
+ if(offset == -1)
+ {
+ SetFailState("Can't find WriteViewAngleUpdateReliableOffset in gamedata.");
+ }
+ gA_ViewAnglePatchAddress = view_as<Address>(addr + view_as<Address>(offset));
+}
+
+void HookEvents()
+{
+ GameData gamedataConf = LoadGameConfigFile("gokz-tpanglefix.games");
+ if (gamedataConf == null)
+ {
+ SetFailState("Failed to load gokz-tpanglefix gamedata");
+ }
+ gH_WriteViewAngleUpdate = DynamicDetour.FromConf(gamedataConf, "CGameClient::WriteViewAngleUpdate");
+
+ if (gH_WriteViewAngleUpdate == INVALID_HANDLE)
+ {
+ SetFailState("Failed to find CGameClient::WriteViewAngleUpdate function signature");
+ }
+
+ if (!gH_WriteViewAngleUpdate.Enable(Hook_Pre, DHooks_OnWriteViewAngleUpdate_Pre))
+ {
+ SetFailState("Failed to enable detour on CGameClient::WriteViewAngleUpdate");
+ }
+ // Prevent the server from crashing.
+ FindConVar("sv_parallel_sendsnapshot").SetBool(false);
+ FindConVar("sv_parallel_sendsnapshot").AddChangeHook(OnParallelSendSnapshotCvarChanged);
+
+ gI_ClientOffset = gamedataConf.GetOffset("ClientIndexOffset");
+ if (gI_ClientOffset == -1)
+ {
+ SetFailState("Failed to get ClientIndexOffset offset.");
+ }
+}
+
+void OnParallelSendSnapshotCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue)
+{
+ if (convar.BoolValue)
+ {
+ convar.BoolValue = false;
+ }
+}
+
+public MRESReturn DHooks_OnWriteViewAngleUpdate_Pre(Address pThis)
+{
+ int client = LoadFromAddress(pThis + view_as<Address>(gI_ClientOffset), NumberType_Int32);
+ if (gB_EnableFix[client])
+ {
+ PatchAngleFix();
+ }
+ else
+ {
+ RestoreAngleFix();
+ }
+ return MRES_Ignored;
+}
+
+void PatchAngleFix()
+{
+ if (LoadFromAddress(gA_ViewAnglePatchAddress, NumberType_Int8) == 0)
+ {
+ StoreToAddress(gA_ViewAnglePatchAddress, 1, NumberType_Int8);
+ }
+}
+
+void RestoreAngleFix()
+{
+ if (LoadFromAddress(gA_ViewAnglePatchAddress, NumberType_Int8) == 1)
+ {
+ StoreToAddress(gA_ViewAnglePatchAddress, 0, NumberType_Int8);
+ }
+}
+
+bool ToggleAngleFix(int client)
+{
+ gB_EnableFix[client] = !gB_EnableFix[client];
+ return gB_EnableFix[client];
+}
+
+public void OnAllPluginsLoaded()
+{
+ if (LibraryExists("updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+ TopMenu topMenu;
+ if (LibraryExists("gokz-core") && ((topMenu = GOKZ_GetOptionsTopMenu()) != null))
+ {
+ GOKZ_OnOptionsMenuReady(topMenu);
+ }
+}
+
+public void OnLibraryAdded(const char[] name)
+{
+ if (StrEqual(name, "updater"))
+ {
+ Updater_AddPlugin(UPDATER_URL);
+ }
+
+}
+
+// =====[ CLIENT EVENTS ]=====
+
+public void GOKZ_OnOptionChanged(int client, const char[] option, any newValue)
+{
+ OnOptionChanged_Options(client, option, newValue);
+}
+
+// =====[ OTHER EVENTS ]=====
+
+public void GOKZ_OnOptionsMenuReady(TopMenu topMenu)
+{
+ OnOptionsMenuReady_Options();
+ OnOptionsMenuReady_OptionsMenu(topMenu);
+}
+
+
+// =====[ OPTIONS ]=====
+
+void OnOptionsMenuReady_Options()
+{
+ RegisterOption();
+}
+
+void RegisterOption()
+{
+ GOKZ_RegisterOption(TPANGLEFIX_OPTION_NAME, TPANGLEFIX_OPTION_DESCRIPTION,
+ OptionType_Int, TPAngleFix_Disabled, 0, TPANGLEFIX_COUNT - 1);
+}
+
+void OnOptionChanged_Options(int client, const char[] option, any newValue)
+{
+ if (StrEqual(option, TPANGLEFIX_OPTION_NAME))
+ {
+ gB_EnableFix[client] = newValue;
+ switch (newValue)
+ {
+ case TPAngleFix_Disabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - TP Angle Fix - Disable");
+ }
+ case TPAngleFix_Enabled:
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Option - TP Angle Fix - Enable");
+ }
+ }
+ }
+}
+
+// =====[ OPTIONS MENU ]=====
+
+void OnOptionsMenuReady_OptionsMenu(TopMenu topMenu)
+{
+ if (gTM_Options == topMenu)
+ {
+ return;
+ }
+
+ gTM_Options = topMenu;
+ gTMO_CatGeneral = gTM_Options.FindCategory(GENERAL_OPTION_CATEGORY);
+ gTMO_ItemTPAngleFix = gTM_Options.AddItem(TPANGLEFIX_OPTION_NAME, TopMenuHandler_TPAngleFix, gTMO_CatGeneral);
+}
+
+public void TopMenuHandler_TPAngleFix(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength)
+{
+ if (topobj_id != gTMO_ItemTPAngleFix)
+ {
+ return;
+ }
+
+ if (action == TopMenuAction_DisplayOption)
+ {
+ if (GOKZ_GetOption(param, TPANGLEFIX_OPTION_NAME) == TPAngleFix_Disabled)
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - TP Angle Fix", param,
+ "Options Menu - Disabled", param);
+ }
+ else
+ {
+ FormatEx(buffer, maxlength, "%T - %T",
+ "Options Menu - TP Angle Fix", param,
+ "Options Menu - Enabled", param);
+ }
+ }
+ else if (action == TopMenuAction_SelectOption)
+ {
+ GOKZ_CycleOption(param, TPANGLEFIX_OPTION_NAME);
+ gTM_Options.Display(param, TopMenuPosition_LastCategory);
+ }
+}
+
+// =====[ COMMANDS ]=====
+
+void RegisterCommands()
+{
+ RegConsoleCmd("sm_tpafix", CommandTPAFix, "[KZ] Toggle teleport angle fix.");
+}
+
+public Action CommandTPAFix(int client, int args)
+{
+ GOKZ_SetOption(client, TPANGLEFIX_OPTION_NAME, ToggleAngleFix(client));
+ return Plugin_Handled;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/GlobalAPI.inc b/sourcemod/scripting/include/GlobalAPI.inc
new file mode 100644
index 0000000..8813645
--- /dev/null
+++ b/sourcemod/scripting/include/GlobalAPI.inc
@@ -0,0 +1,822 @@
+// ================== DOUBLE INCLUDE ========================= //
+
+#if defined _GlobalAPI_included_
+#endinput
+#endif
+#define _GlobalAPI_included_
+
+// ======================= DEFINITIONS ======================= //
+
+#define DEFAULT_DATA 0
+#define DEFAULT_INT -1
+#define DEFAULT_STRING ""
+#define DEFAULT_FLOAT -1.0
+#define DEFAULT_BOOL view_as<bool>(-1)
+
+#define GlobalAPI_Plugin_Version "2.0.0"
+#define GlobalAPI_Plugin_Desc "Plugin helper for GlobalAPI Production & Staging"
+#define GlobalAPI_Plugin_Url "https://bitbucket.org/kztimerglobalteam/GlobalAPI-SMPlugin"
+#define GlobalAPI_Plugin_NameVersion "GlobalAPI Plugin " ... GlobalAPI_Plugin_Version
+
+#define GlobalAPI_Backend_Version "v2.0"
+#define GlobalAPI_Backend_Staging_Version "v2.0"
+#define GlobalAPI_BaseUrl "https://kztimerglobal.com/api/" ... GlobalAPI_Backend_Version
+#define GlobalAPI_Staging_BaseUrl "https://globalapi.ruto.sh/api/" ... GlobalAPI_Backend_Staging_Version
+
+#define GlobalAPI_Max_BaseUrl_Length 128
+#define GlobalAPI_Max_QueryParam_Num 20
+#define GlobalAPI_Max_QueryParam_Length 64
+#define GlobalAPI_Max_QueryParams_Length (GlobalAPI_Max_QueryParam_Num * GlobalAPI_Max_QueryParam_Length)
+#define GlobalAPI_Max_QueryUrl_Length (GlobalAPI_Max_QueryParams_Length + GlobalAPI_Max_BaseUrl_Length)
+#define GlobalAPI_Max_QueryParam_Array_Length 64
+
+#define GlobalAPI_Max_APIKey_Length 128
+#define GlobalAPI_Max_PluginName_Length 64
+#define GlobalAPI_Max_PluginVersion_Length 32
+#define GlobalAPI_Data_File_Extension "GAPI"
+
+// ======================= INCLUDES ========================== //
+
+#include <GlobalAPI/requestdata>
+#include <GlobalAPI/responses>
+#include <GlobalAPI/stocks>
+
+// ======================= ENUMS ============================= //
+
+/**
+ * Defines what request method is used on requests
+ */
+enum
+{
+ GlobalAPIRequestType_GET = 0, /**< Request uses GET HTTP method */
+ GlobalAPIRequestType_POST /**< Request uses POST HTTP method */
+};
+
+/**
+ * Defines what accept type is used on requests
+ */
+enum
+{
+ GlobalAPIRequestAcceptType_JSON = 0, /**< Request uses application/json HTTP accept type */
+ GlobalAPIRequestAcceptType_OctetStream /**< Request uses application/octet-stream HTTP accept type */
+};
+
+/**
+ * Defines what content type is used on requests
+ */
+enum
+{
+ GlobalAPIRequestContentType_JSON = 0, /**< Request uses application/json HTTP content type */
+ GlobalAPIRequestContentType_OctetStream /**< Request uses application/octet-stream HTTP content type */
+};
+
+// ======================= TYPEDEFS ========================== //
+
+/*
+ Function types when API call finishes
+*/
+typeset OnAPICallFinished
+{
+ /**
+ * Called when an API call has finished
+ *
+ * @param hResponse JSON_Object handle to the response
+ * @param hData GlobalAPIRequestData handle for the request
+ * @noreturn
+ */
+ function void(JSON_Object hResponse, GlobalAPIRequestData hData);
+
+ /**
+ * Called when an API call has finished
+ *
+ * @param hResponse JSON_Object handle to the response
+ * @param hData GlobalAPIRequestData handle for the request
+ * @param data Optional data that was passed
+ * @noreturn
+ */
+ function void(JSON_Object hResponse, GlobalAPIRequestData hData, any data);
+};
+
+// ======================= FORWARDS ========================== //
+
+/**
+ * Called when GlobalAPI plugin is initialized,
+ * this means API Key is loaded and all the cvars are loaded
+ *
+ * @noreturn
+ */
+forward void GlobalAPI_OnInitialized();
+
+/**
+ * Called when GlobalAPI plugin has failed a request
+ *
+ * @param request Handle to the request failed
+ * @param hData Handle to request's GlobalAPIRequestData
+ * @noreturn
+ */
+forward void GlobalAPI_OnRequestFailed(Handle request, GlobalAPIRequestData hData);
+
+/**
+ * Called when GlobalAPI plugin has started a request
+ *
+ * @param request Handle to the request started
+ * @param hData Handle to request's GlobalAPIRequestData
+ * @noreturn
+ */
+forward void GlobalAPI_OnRequestStarted(Handle request, GlobalAPIRequestData hData);
+
+/**
+ * Called when GlobalAPI plugin has finished a request
+ *
+ * @param request Handle to the request finished
+ * @param hData Handle to request's GlobalAPIRequestData
+ * @noreturn
+ */
+forward void GlobalAPI_OnRequestFinished(Handle request, GlobalAPIRequestData hData);
+
+// ======================= NATIVES =========================== //
+
+/**
+ * Gets a boolean of whether GlobalAPI plugin is initialized.
+ *
+ * @note See GlobalAPI_OnInitialized for the event version.
+ * @return Whether GlobalAPI plugin is initialized.
+ */
+native bool GlobalAPI_IsInit();
+
+/**
+ * Gets the API Key used by GlobalAPI plugin
+ *
+ * @param buffer Buffer to store result in
+ * @param maxlength Max length of the buffer
+ * @noreturn
+ */
+native void GlobalAPI_GetAPIKey(char[] buffer, int maxlength);
+
+/**
+ * Gets whether GlobalAPI is using an API Key
+ *
+ * @note This does not mean the API Key is valid!
+ * @return Whether API Key is used by GlobalAPI plugin
+ */
+native bool GlobalAPI_HasAPIKey();
+
+/**
+ * Gets whether GlobalAPI is using the staging endpoint
+ *
+ * @note It is not safe to call this before GlobalAPI_OnInitialized!
+ * @return Whether staging endpoint is used by GlobalAPI plugin
+ */
+native bool GlobalAPI_IsStaging();
+
+/**
+ * Gets whether GlobalAPI is in debug mode
+ *
+ * @note It is not safe to call this before GlobalAPI_OnInitialized!
+ * @return Whether GlobalAPI plugin is in debug mode
+ */
+native bool GlobalAPI_IsDebugging();
+
+/**
+ * Sends a request in GlobalAPI plugin format
+ *
+ * @param hData Handle to GlobalAPIRequestData
+ * @return Whether the request was sent successfully
+ */
+native bool GlobalAPI_SendRequest(GlobalAPIRequestData hData);
+
+/**
+ * Sends a debug message to GlobalAPI plugin logs if debugging is enabled
+ *
+ * @param message Formatting rules
+ * @param ... Variable number of format parameters
+ * @note This is not safe to use before convars have loaded
+ * @return Whether the message was logged
+ */
+native bool GlobalAPI_DebugMessage(const char[] message, any ...);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/auth/status
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetAuthStatus(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/bans
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param banTypes Ban types to query
+ * @param banTypesList -Unsupported at the moment-
+ * @param isExpired Whether to query for isExpired or not
+ * @param ipAddress IP address to query
+ * @param steamId64 SteamID64 to query
+ * @param steamId SteamID2 to query
+ * @param notesContain Notes to query
+ * @param statsContain Stats to query
+ * @param serverId Server ID to query
+ * @param createdSince Created since date to query
+ * @param updatedSince Updated since date to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetBans(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] banTypes = DEFAULT_STRING,
+ const char[] banTypesList = DEFAULT_STRING, bool isExpired = DEFAULT_BOOL, const char[] ipAddress = DEFAULT_STRING,
+ const char[] steamId64 = DEFAULT_STRING, const char[] steamId = DEFAULT_STRING, const char[] notesContain = DEFAULT_STRING,
+ const char[] statsContain = DEFAULT_STRING, int serverId = DEFAULT_INT, const char[] createdSince = DEFAULT_STRING,
+ const char[] updatedSince = DEFAULT_STRING, int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a POST HTTP Request to /api/{version}/bans
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 of the user
+ * @param banType Type of the ban
+ * @param stats Stats of the ban
+ * @param notes Notes of the ban
+ * @param ipAddress IP address of the user
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_CreateBan(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ const char[] steamId, const char[] banType, const char[] stats,
+ const char[] notes, const char[] ipAddress);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/jumpstats
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param id Id to query
+ * @param serverId Server id to query
+ * @param steamId64 SteamID64 to query
+ * @param steamId SteamID2 to query
+ * @param jumpType Jump type to query
+ * @param steamId64List -Unsupported at the moment-
+ * @param jumpTypeList -Unsupported at the moment-
+ * @param greaterThanDistance Greater than distance to query
+ * @param lessThanDistance Less than distance to query
+ * @param isMsl Whether to query for isMsl or not
+ * @param isCrouchBind Whether to query for isCrouchBind or not
+ * @param isForwardBind Whether to query for isForwardBind or not
+ * @param isCrouchBoost Whether to query for isCrouchBoost or not
+ * @param updatedById Updated by id to query
+ * @param createdSince Created since date to query
+ * @param updatedSince Updated since date to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetJumpstats(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id = DEFAULT_INT,
+ int serverId = DEFAULT_INT, const char[] steamId64 = DEFAULT_STRING, const char[] steamId = DEFAULT_STRING,
+ const char[] jumpType = DEFAULT_STRING, const char[] steamId64List = DEFAULT_STRING,
+ const char[] jumpTypeList = DEFAULT_STRING, float greaterThanDistance = DEFAULT_FLOAT,
+ float lessThanDistance = DEFAULT_FLOAT, bool isMsl = DEFAULT_BOOL, bool isCrouchBind = DEFAULT_BOOL,
+ bool isForwardBind = DEFAULT_BOOL, bool isCrouchBoost = DEFAULT_BOOL, int updatedById = DEFAULT_INT,
+ const char[] createdSince = DEFAULT_STRING, const char[] updatedSince = DEFAULT_STRING,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a POST HTTP Request to /api/{version}/jumpstats
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 of the user
+ * @param jumpType Type of the jump
+ * @param distance Distance of the jump
+ * @param jumpJsonInfo Data of the jump
+ * @param tickRate Tickrate of the server
+ * @param mslCount Msl count of the jump
+ * @param isCrouchBind Whether crouch bind was used
+ * @param isForwardBind Whether forward bind was used
+ * @param isCrouchBoost Whether crouch boost was used
+ * @param strafeCount Strafe count of the jump
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_CreateJumpstat(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId,
+ int jumpType, float distance, const char[] jumpJsonInfo, int tickRate, int mslCount,
+ bool isCrouchBind, bool isForwardBind, bool isCrouchBoost, int strafeCount);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/jumpstats/{jump_type}/top
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param jumpType Jump type to query
+ * @param id Id to query
+ * @param serverId Server Id to query
+ * @param steamId64 SteamID64 to query
+ * @param steamId SteamID2 to query
+ * @param steamId64List -Unsupported at the moment-
+ * @param jumpTypeList -Unsupported at the moment-
+ * @param greaterThanDistance Greater than distance to query
+ * @param lessThanDistance Less than distance to query
+ * @param isMsl Whether to query for isMsl or not
+ * @param isCrouchBind Whether to query for isCrouchBind or not
+ * @param isForwardBind Whether to query for isForwardBind or not
+ * @param isCrouchBoost Whether to query for isCrouchBoost or not
+ * @param updatedById Updated by id to query
+ * @param createdSince Created since date to query
+ * @param updatedSince Updated since date to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetJumpstatTop(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] jumpType,
+ int id = DEFAULT_INT, int serverId = DEFAULT_INT, const char[] steamId64 = DEFAULT_STRING,
+ const char[] steamId = DEFAULT_STRING, const char[] steamId64List = DEFAULT_STRING,
+ const char[] jumpTypeList = DEFAULT_STRING, float greaterThanDistance = DEFAULT_FLOAT,
+ float lessThanDistance = DEFAULT_FLOAT, bool isMsl = DEFAULT_BOOL, bool isCrouchBind = DEFAULT_BOOL,
+ bool isForwardBind = DEFAULT_BOOL, bool isCrouchBoost = DEFAULT_BOOL, int updatedById = DEFAULT_INT,
+ const char[] createdSince = DEFAULT_STRING, const char[] updatedSince = DEFAULT_STRING,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/jumpstats/{jump_type}/top30
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param jumpType Jump type to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetJumpstatTop30(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] jumpType);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/maps
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param name Map name to query
+ * @param largerThanFilesize Larger than filesize to query
+ * @param smallerThanFilesize Smaller than filesize to query
+ * @param isValidated Whether to query for isValidated or not
+ * @param difficulty Map difficulty to query
+ * @param createdSince Created since date to query
+ * @param updatedSince Updated since date to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetMaps(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] name = DEFAULT_STRING,
+ int largerThanFilesize = DEFAULT_INT, int smallerThanFilesize = DEFAULT_INT, bool isValidated = DEFAULT_BOOL,
+ int difficulty = DEFAULT_INT, const char[] createdSince = DEFAULT_STRING, const char[] updatedSince = DEFAULT_STRING,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/maps/{id}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param id Map id to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetMapById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/maps/name/{map_name}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param name Map name to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetMapByName(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] name);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/modes
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetModes(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/modes/id/{id}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param id Mode id to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetModeById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/modes/name/{mode_name}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param name Mode name to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetModeByName(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] name);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/players
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 to query
+ * @param isBanned Whether to query for isBanned or not
+ * @param totalRecords Total records to query
+ * @param ipAddress IP address to query
+ * @param steamId64List -Unsupported at the moment-
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetPlayers(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId = DEFAULT_STRING,
+ bool isBanned = DEFAULT_BOOL, int totalRecords = DEFAULT_INT, const char[] ipAddress = DEFAULT_STRING,
+ const char[] steamId64List = DEFAULT_STRING);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/players/steamid/{steamid}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetPlayerBySteamId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/players/steamid/{steamid}/ip/{ip}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 to query
+ * @param ipAddress IP address to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetPlayerBySteamIdAndIp(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ const char[] steamId, const char[] ipAddress);
+
+/**
+ * Starts a POST HTTP Request to /api/{version}/records
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 of the user
+ * @param mapId Map id of the record
+ * @param mode Mode of the record
+ * @param stage Stage of the record
+ * @param tickRate Tickrate of the server
+ * @param teleports Teleport count of the record
+ * @param time Elapsed time of the record
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_CreateRecord(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] steamId,
+ int mapId, const char[] mode, int stage, int tickRate, int teleports, float time);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/place/{id}
+ *
+ * @note This is deprecated!
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param id Id to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetRecordPlaceById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/top
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 to query
+ * @param steamId64 SteamID64 to query
+ * @param mapId Map id to query
+ * @param mapName Map name to query
+ * @param tickRate Tickrate to query
+ * @param stage Stage to query
+ * @param modes Mode(s) to query
+ * @param hasTeleports Whether to query for hasTeleports or not
+ * @param playerName Player name to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetRecordsTop(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ const char[] steamId = DEFAULT_STRING, const char[] steamId64 = DEFAULT_STRING, int mapId = DEFAULT_INT,
+ const char[] mapName = DEFAULT_STRING, int tickRate = DEFAULT_INT, int stage = DEFAULT_INT,
+ const char[] modes = DEFAULT_STRING, bool hasTeleports = DEFAULT_BOOL,
+ const char[] playerName = DEFAULT_STRING, int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/top/recent
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param steamId SteamID2 to query
+ * @param steamId64 SteamID64 to query
+ * @param mapId Map id to query
+ * @param mapName Map name to query
+ * @param tickRate Tickrate to query
+ * @param stage Stage to query
+ * @param modes Mode(s) to query
+ * @param topAtLeast Place top at least to query
+ * @param topOverallAtLeast Place top overall at least to query
+ * @param hasTeleports Whether to query for hasTeleports or not
+ * @param createdSince Created since date to query
+ * @param playerName Player name to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetRecordsTopRecent(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ const char[] steamId = DEFAULT_STRING, const char[] steamId64 = DEFAULT_STRING,
+ int mapId = DEFAULT_INT, const char[] mapName = DEFAULT_STRING,
+ int tickRate = DEFAULT_INT, int stage = DEFAULT_INT,
+ const char[] modes = DEFAULT_STRING, int topAtLeast = DEFAULT_INT,
+ int topOverallAtLeast = DEFAULT_INT, bool hasTeleports = DEFAULT_BOOL,
+ const char[] createdSince = DEFAULT_STRING, const char[] playerName = DEFAULT_STRING,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/top/world_records
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param ids Array of ids to query
+ * @param idsLength Length of the ids array
+ * @param mapIds Array of map ids to query
+ * @param mapIdsLength Length of the map ids array
+ * @param stages Array of stages to query
+ * @param stagesLength Length of the stages array
+ * @param modeIds Array of mode ids to query
+ * @param modeIdsLength Length of the mode ids array
+ * @param tickRates Array of tickrates to query
+ * @param tickRatesLength Length of the tickrates array
+ * @param hasTeleports Whether to query for hasTeleports or not
+ * @param mapTag Map tags to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetRecordsTopWorldRecords(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int[] ids = {}, int idsLength = DEFAULT_INT,
+ int[] mapIds = {}, int mapIdsLength = DEFAULT_INT,
+ int[] stages = {}, int stagesLength = DEFAULT_INT,
+ int[] modeIds = {}, int modeIdsLength = DEFAULT_INT,
+ int[] tickRates = {}, int tickRatesLength = DEFAULT_INT,
+ bool hasTeleports = DEFAULT_BOOL, char[] mapTag = DEFAULT_STRING,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/servers
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param id Id to query
+ * @param port Port to query
+ * @param ip IP address to query
+ * @param name Server name to query
+ * @param ownerSteamId64 Owner's steamid64 to query
+ * @param approvalStatus Approval status to query
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetServers(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int id = DEFAULT_INT, int port = DEFAULT_INT, const char[] ip = DEFAULT_STRING,
+ const char[] name = DEFAULT_STRING, const char[] ownerSteamId64 = DEFAULT_STRING,
+ int approvalStatus = DEFAULT_INT, int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/servers/{id}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param id Id to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetServerById(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int id);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/servers/name/{server_name}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param serverName Server name to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetServersByName(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, const char[] serverName);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/player_ranks
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param pointsGreaterThan Points greater than to query
+ * @param averageGreaterThan Average greater than to query
+ * @param ratingGreaterThan Rating greater than to query
+ * @param finishesGreaterThan Finishes greater than to query
+ * @param steamId64List Comma-separated stirng of steamid64s to query
+ * @param recordFilterIds Array of record filter ids to query
+ * @param recordFilterIdsLength Length of the record filter ids array
+ * @param mapIds Array of map ids to query
+ * @param mapIdsLength Length of the map ids array
+ * @param stages Array of stages to query
+ * @param stagesLength Length of the stages array
+ * @param modeIds Array of mode ids to query
+ * @param modeIdsLength Length of the mode ids array
+ * @param tickRates Array of tickrates to query
+ * @param tickRatesLength Length of the tickrates array
+ * @param hasTeleports Whether to query for hasTeleports or not
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetPlayerRanks(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int pointsGreaterThan = DEFAULT_INT, float averageGreaterThan = DEFAULT_FLOAT,
+ float ratingGreaterThan = DEFAULT_FLOAT, int finishesGreaterThan = DEFAULT_INT,
+ const char[] steamId64List = DEFAULT_STRING,
+ int[] recordFilterIds = {}, int recordFilterIdsLength = DEFAULT_INT,
+ int[] mapIds = {}, int mapIdsLength = DEFAULT_INT,
+ int[] stages = {}, int stagesLength = DEFAULT_INT,
+ int[] modeIds = {}, int modeIdsLength = DEFAULT_INT,
+ int[] tickRates = {}, int tickRatesLength = DEFAULT_INT,
+ bool hasTeleports = DEFAULT_BOOL, int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/record_filters
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param ids Array of ids to query
+ * @param idsLength Length of the ids array
+ * @param mapIds Array of map ids to query
+ * @param mapIdsLength Length of the map ids array
+ * @param stages Array of stages to query
+ * @param stagesLength Length of the stages array
+ * @param modeIds Array of mode ids to query
+ * @param modeIdsLength Length of the mode ids array
+ * @param tickRates Array of tickrates to query
+ * @param tickRatesLength Length of the tickrates array
+ * @param hasTeleports Whether to query for hasTeleports or not
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetRecordFilters(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int[] ids = {}, int idsLength = DEFAULT_INT,
+ int[] mapIds = {}, int mapIdsLength = DEFAULT_INT,
+ int[] stages = {}, int stagesLength = DEFAULT_INT,
+ int[] modeIds = {}, int modeIdsLength = DEFAULT_INT,
+ int[] tickRates = {}, int tickRatesLength = DEFAULT_INT,
+ bool hasTeleports = DEFAULT_BOOL, bool isOverall = DEFAULT_BOOL,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/record_filters/distributions
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param ids Array of ids to query
+ * @param idsLength Length of the ids array
+ * @param mapIds Array of map ids to query
+ * @param mapIdsLength Length of the map ids array
+ * @param stages Array of stages to query
+ * @param stagesLength Length of the stages array
+ * @param modeIds Array of mode ids to query
+ * @param modeIdsLength Length of the mode ids array
+ * @param tickRates Array of tickrates to query
+ * @param tickRatesLength Length of the tickrates array
+ * @param hasTeleports Whether to query for hasTeleports or not
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetRecordFilterDistributions(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int[] ids = {}, int idsLength = DEFAULT_INT,
+ int[] mapIds = {}, int mapIdsLength = DEFAULT_INT,
+ int[] stages = {}, int stagesLength = DEFAULT_INT,
+ int[] modeIds = {}, int modeIdsLength = DEFAULT_INT,
+ int[] tickRates = {}, int tickRatesLength = DEFAULT_INT,
+ bool hasTeleports = DEFAULT_BOOL, bool isOverall = DEFAULT_BOOL,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/replay/list
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param offset Offset of the dataset to query
+ * @param limit Amount of items returned for the query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetReplayList(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int offset = DEFAULT_INT, int limit = DEFAULT_INT);
+
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/{recordId}/replay
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param recordId Record id to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetReplayByRecordId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int recordId);
+/**
+ * Starts a GET HTTP Request to /api/{version}/records/replay/{replayId}
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param replayId Replay id to query
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_GetReplayByReplayId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA, int replayId);
+
+/**
+ * Starts a POST HTTP Request to /api/{version}/records/{recordId}/replay
+ *
+ * @param callback Callback when request has finished
+ * @param data Optional data to pass
+ * @param recordId Id of the record
+ * @param replayFile Path to the replay file
+ * @return Whether request was successfully sent
+ */
+native bool GlobalAPI_CreateReplayForRecordId(OnAPICallFinished callback = INVALID_FUNCTION, any data = DEFAULT_DATA,
+ int recordId, const char[] replayFile);
+
+// ======================= PLUGIN INFO ======================= //
+
+public SharedPlugin __pl_GlobalAPI =
+{
+ name = "GlobalAPI",
+ file = "GlobalAPI.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_GlobalAPI_SetNTVOptional()
+{
+ // Plugin
+ MarkNativeAsOptional("GlobalAPI_IsInit");
+ MarkNativeAsOptional("GlobalAPI_GetAPIKey");
+ MarkNativeAsOptional("GlobalAPI_HasAPIKey");
+ MarkNativeAsOptional("GlobalAPI_IsStaging");
+ MarkNativeAsOptional("GlobalAPI_IsDebugging");
+ MarkNativeAsOptional("GlobalAPI_SendRequest");
+ MarkNativeAsOptional("GlobalAPI_DebugMessage");
+
+ // Auth
+ MarkNativeAsOptional("GlobalAPI_GetAuthStatus");
+
+ // Bans
+ MarkNativeAsOptional("GlobalAPI_GetBans");
+ MarkNativeAsOptional("GlobalAPI_CreateBan");
+
+ // Jumpstats
+ MarkNativeAsOptional("GlobalAPI_GetJumpstats");
+ MarkNativeAsOptional("GlobalAPI_GetJumpstatTop");
+ MarkNativeAsOptional("GlobalAPI_GetJumpstatTop30");
+
+ // Maps
+ MarkNativeAsOptional("GlobalAPI_GetMaps");
+ MarkNativeAsOptional("GlobalAPI_GetMapById");
+ MarkNativeAsOptional("GlobalAPI_GetMapByName");
+
+ // Modes
+ MarkNativeAsOptional("GlobalAPI_GetModes");
+ MarkNativeAsOptional("GlobalAPI_GetModeById");
+ MarkNativeAsOptional("GlobalAPI_GetModeByName");
+
+ // Players
+ MarkNativeAsOptional("GlobalAPI_GetPlayers");
+ MarkNativeAsOptional("GlobalAPI_GetPlayerBySteamId");
+ MarkNativeAsOptional("GlobalAPI_GetPlayerBySteamIdAndIp");
+
+ // Records
+ MarkNativeAsOptional("GlobalAPI_CreateRecord");
+ MarkNativeAsOptional("GlobalAPI_GetRecordPlaceById");
+ MarkNativeAsOptional("GlobalAPI_GetRecordsTop");
+ MarkNativeAsOptional("GlobalAPI_GetRecordsTopRecent");
+ MarkNativeAsOptional("GlobalAPI_GetRecordsTopWorldRecords");
+
+ // Servers
+ MarkNativeAsOptional("GlobalAPI_GetServers");
+ MarkNativeAsOptional("GlobalAPI_GetServerById");
+ MarkNativeAsOptional("GlobalAPI_GetServersByName");
+
+ // Ranks
+ MarkNativeAsOptional("GlobalAPI_GetPlayerRanks");
+
+ // Record Filters
+ MarkNativeAsOptional("GlobalAPI_GetRecordFilters");
+ MarkNativeAsOptional("GlobalAPI_GetRecordFilterDistributions");
+
+ // Replays
+ MarkNativeAsOptional("GlobalAPI_GetReplayList");
+ MarkNativeAsOptional("GlobalAPI_GetReplayByRecordId");
+ MarkNativeAsOptional("GlobalAPI_GetReplayByReplayId");
+ MarkNativeAsOptional("GlobalAPI_CreateReplayForRecordId");
+}
+#endif
diff --git a/sourcemod/scripting/include/GlobalAPI/iterable.inc b/sourcemod/scripting/include/GlobalAPI/iterable.inc
new file mode 100644
index 0000000..585873b
--- /dev/null
+++ b/sourcemod/scripting/include/GlobalAPI/iterable.inc
@@ -0,0 +1,55 @@
+// ================== DOUBLE INCLUDE ========================= //
+
+#if defined _GlobalAPI_Iterable_included_
+#endinput
+#endif
+#define _GlobalAPI_Iterable_included_
+
+// =========================================================== //
+
+#include <json>
+
+// =========================================================== //
+
+/*
+ Helper methodmap for JSON_Object arrays
+*/
+methodmap APIIterable < JSON_Object
+{
+ /**
+ * Creates a new APIIterable
+ *
+ * @param hItems JSON_Object array handle
+ * @return A new APIIterable handle
+ */
+ public APIIterable(JSON_Object hItems)
+ {
+ if (hItems.HasKey("result"))
+ {
+ return view_as<APIIterable>(hItems.GetObject("result"));
+ }
+ return view_as<APIIterable>(hItems);
+ }
+
+ /*
+ Gets count of the items in the array
+ */
+ property int Count
+ {
+ public get() { return this.Length; }
+ }
+
+ /**
+ * Gets an object from the array by index
+ *
+ * @note This is an alias to GetObjectIndexed
+ * @param index Index of the object we want to retrieve
+ * @return JSON_Object handle to the object retrieved
+ */
+ public JSON_Object GetById(int index)
+ {
+ return this.GetObjectIndexed(index);
+ }
+}
+
+// =========================================================== // \ No newline at end of file
diff --git a/sourcemod/scripting/include/GlobalAPI/request.inc b/sourcemod/scripting/include/GlobalAPI/request.inc
new file mode 100644
index 0000000..e683125
--- /dev/null
+++ b/sourcemod/scripting/include/GlobalAPI/request.inc
@@ -0,0 +1,185 @@
+// ================== DOUBLE INCLUDE ========================= //
+
+#if defined _GlobalAPI_Request_included_
+#endinput
+#endif
+#define _GlobalAPI_Request_included_
+
+// =========================================================== //
+
+static char gC_acceptTypePhrases[][] =
+{
+ "application/json",
+ "application/octet-stream"
+};
+
+static char gC_contentTypePhrases[][] =
+{
+ "application/json",
+ "application/octet-stream"
+};
+
+// =========================================================== //
+
+methodmap GlobalAPIRequest < Handle
+{
+ /**
+ * Creates a new GlobalAPIRequest
+ *
+ * @param url URL of the request
+ * @param method SteamWorks k_ETTPMethod of the request
+ * @return A new GlobalAPIRequest handle
+ */
+ public GlobalAPIRequest(char[] url, EHTTPMethod method)
+ {
+ Handle request = SteamWorks_CreateHTTPRequest(method, url);
+ return view_as<GlobalAPIRequest>(request);
+ }
+
+ /**
+ * Sets request timeout
+ *
+ * @param seconds Timeout in seconds
+ * @return Whether the operation was successful
+ */
+ public bool SetTimeout(int seconds)
+ {
+ return SteamWorks_SetHTTPRequestAbsoluteTimeoutMS(this, seconds * 1000);
+ }
+
+ /**
+ * Sets request body
+ *
+ * @param hData GlobalAPIRequestData containing contentType
+ * @param body Request body to set
+ * @param maxlength Maxlength of the body
+ * @return Whether the operation was successful
+ */
+ public bool SetBody(GlobalAPIRequestData hData, char[] body, int maxlength)
+ {
+ return SteamWorks_SetHTTPRequestRawPostBody(this, gC_contentTypePhrases[hData.ContentType], body, maxlength);
+ }
+
+ /**
+ * Sets request body from a file
+ *
+ * @param hData GlobalAPIRequestData containing contentType
+ * @return Whether the operation was successful
+ */
+ public bool SetBodyFromFile(GlobalAPIRequestData hData, char[] file)
+ {
+ return SteamWorks_SetHTTPRequestRawPostBodyFromFile(this, gC_contentTypePhrases[hData.ContentType], file);
+ }
+
+ /**
+ * Sets a request context value
+ *
+ * @param data Any data to pass
+ * @return Whether the operation was successful
+ */
+ public bool SetData(any data1, any data2 = 0)
+ {
+ return SteamWorks_SetHTTPRequestContextValue(this, data1, data2);
+ }
+
+ /**
+ * Sets predefined HTTP callbacks
+ *
+ * @note Predefined values respectively:
+ * @note Global_HTTP_Completed, Global_HTTP_Headers and Global_HTTP_DataReceived
+ * @noreturn
+ */
+ public void SetCallbacks()
+ {
+ SteamWorks_SetHTTPCallbacks(this, Global_HTTP_Completed, Global_HTTP_Headers, Global_HTTP_DataReceived);
+ }
+
+ /**
+ * Sets "Accept" header
+ *
+ * @param hData GlobalAPIRequestData containing acceptType
+ * @return Whether the operation was successful
+ */
+ public bool SetAcceptHeaders(GlobalAPIRequestData hData)
+ {
+ return SteamWorks_SetHTTPRequestHeaderValue(this, "Accept", gC_acceptTypePhrases[hData.AcceptType]);
+ }
+
+ /**
+ * Sets "powered by" header
+ *
+ * @return Whether the operation was successful
+ */
+ public bool SetPoweredByHeader()
+ {
+ return SteamWorks_SetHTTPRequestHeaderValue(this, "X-Powered-By", GlobalAPI_Plugin_NameVersion);
+ }
+
+ /**
+ * Sets authentication header
+ *
+ * @return Whether the operation was successful
+ */
+ public bool SetAuthenticationHeader(char[] apiKey)
+ {
+ return SteamWorks_SetHTTPRequestHeaderValue(this, "X-ApiKey", apiKey);
+ }
+
+ /**
+ * Sets envinroment headers (MetaMod & SourceMod)
+ *
+ * @param mmVersion MetaMod version string
+ * @param smVersion SourceMod version string
+ * @return Whether the operation was successful
+ */
+ public bool SetEnvironmentHeaders(char[] mmVersion, char[] smVersion)
+ {
+ return SteamWorks_SetHTTPRequestHeaderValue(this, "X-MetaMod-Version", mmVersion)
+ && SteamWorks_SetHTTPRequestHeaderValue(this, "X-SourceMod-Version", smVersion);
+ }
+
+ /**
+ * Sets content type header
+ *
+ * @param hData GlobalAPIRequestData containing contentType
+ * @return Whether the operation was successful
+ */
+ public bool SetContentTypeHeader(GlobalAPIRequestData hData)
+ {
+ return SteamWorks_SetHTTPRequestHeaderValue(this, "Content-Type", gC_contentTypePhrases[hData.ContentType]);
+ }
+
+ /**
+ * Sets request origin header
+ *
+ * @param hData GlobalAPIRequestData containing pluginName
+ * @return Whether the operation was successful
+ */
+ public bool SetRequestOriginHeader(GlobalAPIRequestData hData)
+ {
+ char pluginName[GlobalAPI_Max_PluginName_Length];
+ hData.GetString("pluginName", pluginName, sizeof(pluginName));
+
+ char pluginVersion[GlobalAPI_Max_PluginVersion_Length + 2];
+ hData.GetString("pluginVersion", pluginVersion, sizeof(pluginVersion));
+
+ char fullPluginDisplay[sizeof(pluginName) + sizeof(pluginVersion) + 6];
+ Format(fullPluginDisplay, sizeof(fullPluginDisplay), "%s (V.%s)", pluginName, pluginVersion);
+
+ return SteamWorks_SetHTTPRequestHeaderValue(this, "X-Request-Origin", fullPluginDisplay);
+ }
+
+ /**
+ * Sends our request with all available data
+ *
+ * @param hData GlobalAPIRequestData handle with all required keys
+ * @return Whether the operation was successful
+ */
+ public bool Send(GlobalAPIRequestData hData)
+ {
+ Call_Private_OnHTTPStart(this, hData);
+ return SteamWorks_SendHTTPRequest(this);
+ }
+}
+
+// =========================================================== // \ No newline at end of file
diff --git a/sourcemod/scripting/include/GlobalAPI/requestdata.inc b/sourcemod/scripting/include/GlobalAPI/requestdata.inc
new file mode 100644
index 0000000..f055ee8
--- /dev/null
+++ b/sourcemod/scripting/include/GlobalAPI/requestdata.inc
@@ -0,0 +1,534 @@
+// ================== DOUBLE INCLUDE ========================= //
+
+#if defined _GlobalAPI_RequestData_included_
+#endinput
+#endif
+#define _GlobalAPI_RequestData_included_
+
+// =========================================================== //
+
+#include <json>
+
+// =========================================================== //
+
+/*
+ Helper methodmap for wrapping data related to requests
+*/
+methodmap GlobalAPIRequestData < JSON_Object
+{
+ /**
+ * Creates a new GlobalAPIRequestData
+ *
+ * @note You can pass a plugin handle or name and/or version
+ * @note Plugin handle is always preferred
+ * @param plugin Handle to calling plugin
+ * @param pluginName Name of the calling plugin
+ * @param pluginVersion Version of the calling plugin
+ * @return A new GlobalAPIRequestData handle
+ */
+ public GlobalAPIRequestData(Handle plugin = null, char[] pluginName = "Unknown", char[] pluginVersion = "Unknown")
+ {
+ JSON_Object requestData = new JSON_Object();
+
+ if (plugin == null)
+ {
+ requestData.SetString("pluginName", pluginName);
+ requestData.SetString("pluginVersion", pluginVersion);
+ }
+ else
+ {
+ requestData.SetString("pluginName", GetPluginDisplayName(plugin));
+ requestData.SetString("pluginVersion", GetPluginVersion(plugin));
+ }
+
+ requestData.SetKeyHidden("pluginName", true);
+ requestData.SetKeyHidden("pluginVersion", true);
+
+ requestData.SetInt("acceptType", 0);
+ requestData.SetKeyHidden("acceptType", true);
+
+ requestData.SetInt("contentType", 0);
+ requestData.SetKeyHidden("contentType", true);
+
+ return view_as<GlobalAPIRequestData>(requestData);
+ }
+
+ /**
+ * Sets a key as default
+ *
+ * @note This sets them as "Handle" type
+ * @note - See GlobalAPI.inc for default values
+ * @param key Key to set as default
+ * @noreturn
+ */
+ public void SetDefault(char[] key)
+ {
+ this.SetHandle(key);
+ this.SetKeyHidden(key, true);
+ }
+
+ /**
+ * Sets url to the request data
+ *
+ * @param url Url to set
+ * @noreturn
+ */
+ public void AddUrl(char[] url)
+ {
+ this.SetString("url", url);
+ this.SetKeyHidden("url", true);
+ }
+
+ /**
+ * Sets endpoint to the request data
+ *
+ * @param endpoint Endpoint to set
+ * @noreturn
+ */
+ public void AddEndpoint(char[] endpoint)
+ {
+ this.SetString("endpoint", endpoint);
+ this.SetKeyHidden("endpoint", true);
+ }
+
+ /**
+ * Sets body file path to the request data
+ *
+ * @note Path to file with data to be posted
+ * @param path Body file (path) to set
+ * @noreturn
+ */
+ public void AddBodyFile(char[] path)
+ {
+ this.SetString("bodyFile", path);
+ this.SetKeyHidden("bodyFile", true);
+ }
+
+ /**
+ * Sets data file path to the request data
+ *
+ * @note Path for downloaded files
+ * @param path Data path to set
+ * @noreturn
+ */
+ public void AddDataPath(char[] path)
+ {
+ this.SetString("dataFilePath", path);
+ this.SetKeyHidden("dataFilePath", true);
+ }
+
+ /*
+ Get or set the request's "acceptType"
+ */
+ property int AcceptType
+ {
+ public get()
+ {
+ return this.GetInt("acceptType");
+ }
+ public set(int type)
+ {
+ this.SetInt("acceptType", type);
+ }
+ }
+
+ /*
+ Get or set the request's "contentType"
+ */
+ property int ContentType
+ {
+ public get()
+ {
+ return this.GetInt("contentType");
+ }
+ public set(int type)
+ {
+ this.SetInt("contentType", type);
+ }
+ }
+
+ /*
+ Get or set the request's "keyRequired"
+ */
+ property bool KeyRequired
+ {
+ public get()
+ {
+ return this.GetBool("keyRequired");
+ }
+ public set(bool required)
+ {
+ this.SetBool("keyRequired", required);
+ this.SetKeyHidden("keyRequired", true);
+ }
+ }
+
+ /*
+ Get or set the request's "isRetried"
+ */
+ property bool IsRetried
+ {
+ public get()
+ {
+ return this.GetBool("isRetried");
+ }
+ public set(bool retried)
+ {
+ this.SetBool("isRetried", retried);
+ this.SetKeyHidden("isRetried", true);
+ }
+ }
+
+ /*
+ Get or set the request's "bodyLength"
+ */
+ property int BodyLength
+ {
+ public get()
+ {
+ return this.GetInt("bodyLength");
+ }
+ public set(int length)
+ {
+ this.SetInt("bodyLength", length);
+ this.SetKeyHidden("bodyLength", true);
+ }
+ }
+
+ /*
+ Get or set the request's "status"
+ */
+ property int Status
+ {
+ public get()
+ {
+ return this.GetInt("status");
+ }
+ public set(int status)
+ {
+ this.SetInt("status", status);
+ this.SetKeyHidden("status", true);
+ }
+ }
+
+ /*
+ Get or set the request's "responseTime"
+ */
+ property int ResponseTime
+ {
+ public get()
+ {
+ return this.GetInt("responseTime");
+ }
+ public set(int responseTime)
+ {
+ this.SetInt("responseTime", responseTime);
+ this.SetKeyHidden("responseTime", true);
+ }
+ }
+
+ /*
+ Get or set the request's "requestType"
+ */
+ property int RequestType
+ {
+ public get()
+ {
+ return this.GetInt("requestType");
+ }
+ public set(int type)
+ {
+ this.SetInt("requestType", type);
+ this.SetKeyHidden("requestType", true);
+ }
+ }
+
+ /*
+ Get or set the request's "failure"
+ */
+ property bool Failure
+ {
+ public get()
+ {
+ return this.GetBool("failure");
+ }
+ public set(bool failure)
+ {
+ this.SetBool("failure", failure);
+ this.SetKeyHidden("failure", true);
+ }
+ }
+
+ /*
+ Get or set the request's "callback"
+ */
+ property Handle Callback
+ {
+ public get()
+ {
+ return view_as<Handle>(this.GetInt("callback"));
+ }
+ public set(Handle hFwd)
+ {
+ this.SetHandle("callback", hFwd);
+ this.SetKeyType("callback", Type_Int);
+ this.SetKeyHidden("callback", true);
+ }
+ }
+
+ /*
+ Get or set the request's "data"
+ */
+ property any Data
+ {
+ public get()
+ {
+ return this.GetInt("data");
+ }
+ public set(any data)
+ {
+ this.SetInt("data", data);
+ this.SetKeyHidden("data", true);
+ }
+ }
+
+ /**
+ * Adds a number to the request data
+ *
+ * @note Default values are added as "defaults"
+ * @note See GlobalAPI.inc for the default values
+ * @param key Key name to set
+ * @param value Value of the key
+ * @noreturn
+ */
+ public void AddNum(char[] key, int value)
+ {
+ if (value == -1)
+ {
+ this.SetDefault(key);
+ }
+ else
+ {
+ this.SetInt(key, value);
+ }
+ }
+
+ /**
+ * Adds a float to the request data
+ *
+ * @note Default values are added as "defaults"
+ * @note See GlobalAPI.inc for the default values
+ * @param key Key name to set
+ * @param value Value of the key
+ * @noreturn
+ */
+ public void AddFloat(char[] key, float value)
+ {
+ if (value == -1.000000)
+ {
+ this.SetDefault(key);
+ }
+ else
+ {
+ this.SetFloat(key, value);
+ }
+ }
+
+ /**
+ * Adds a string to the request data
+ *
+ * @note Default values are added as "defaults"
+ * @note See GlobalAPI.inc for the default values
+ * @param key Key name to set
+ * @param value Value of the key
+ * @noreturn
+ */
+ public void AddString(char[] key, char[] value)
+ {
+ if (StrEqual(value, ""))
+ {
+ this.SetDefault(key);
+ }
+ else
+ {
+ this.SetString(key, value);
+ }
+ }
+
+ /**
+ * Adds a boolean to the request data
+ *
+ * @note Default values are added as "defaults"
+ * @note See GlobalAPI.inc for the default values
+ * @param key Key name to set
+ * @param value Value of the key
+ * @noreturn
+ */
+ public void AddBool(char[] key, bool value)
+ {
+ if (value != true && value != false)
+ {
+ this.SetDefault(key);
+ }
+ else
+ {
+ this.SetBool(key, value);
+ }
+ }
+
+ /**
+ * Adds integer array to the request data
+ *
+ * @note Max length <= 0 are added as defaults
+ * @param key Key name to set
+ * @param value Values (array) of the key
+ * @param maxlength Max length of the values array
+ * @noreturn
+ */
+ public void AddIntArray(char[] key, int[] value, int maxlength)
+ {
+ if (maxlength <= 0)
+ {
+ this.SetDefault(key);
+ }
+ else
+ {
+ JSON_Object hArray = new JSON_Object(true);
+
+ for (int i = 0; i < maxlength; i++)
+ {
+ hArray.PushInt(value[i]);
+ }
+
+ this.SetObject(key, hArray);
+ }
+ }
+
+ /**
+ * Adds string array to the request data
+ *
+ * @note Item count <= 0 are added as defaults
+ * @param key Key name to set
+ * @param itemCount Amount of strings in the array
+ * @noreturn
+ */
+ public void AddStringArray(char[] key, char[][] value, int itemCount)
+ {
+ if (itemCount <= 0)
+ {
+ this.SetDefault(key);
+ }
+ else
+ {
+ JSON_Object hArray = new JSON_Object(true);
+
+ for (int i = 0; i < itemCount; i++)
+ {
+ hArray.PushString(value[i]);
+ }
+
+ this.SetObject(key, hArray);
+ }
+ }
+
+ /**
+ * Converts all of the request data into a query string representation
+ *
+ * @note This ignores "hidden" keys
+ * @param queryString Buffer to store the result in
+ * @param maxlength Max length of the buffer
+ * @noreturn
+ */
+ public void ToString(char[] queryString, int maxlength)
+ {
+ StringMapSnapshot paramsMap = this.Snapshot();
+
+ char key[64];
+ char value[1024];
+
+ int paramCount = 0;
+
+ for (int i = 0; i < paramsMap.Length; i++)
+ {
+ paramsMap.GetKey(i, key, sizeof(key));
+ if (this.GetKeyHidden(key) || json_is_meta_key(key))
+ {
+ continue;
+ }
+
+ switch(this.GetKeyType(key))
+ {
+ case Type_String:
+ {
+ this.GetString(key, value, sizeof(value));
+ AppendToQueryString(paramCount, queryString, maxlength, key, value);
+ }
+ case Type_Float:
+ {
+ float temp = this.GetFloat(key);
+ FloatToString(temp, value, sizeof(value));
+ AppendToQueryString(paramCount, queryString, maxlength, key, value);
+ }
+ case Type_Int:
+ {
+ int temp = this.GetInt(key);
+ IntToString(temp, value, sizeof(value));
+ AppendToQueryString(paramCount, queryString, maxlength, key, value);
+ }
+ case Type_Bool:
+ {
+ bool temp = this.GetBool(key);
+ BoolToString(temp, value, sizeof(value));
+ AppendToQueryString(paramCount, queryString, maxlength, key, value);
+ }
+ case Type_Object:
+ {
+ JSON_Object hObject = this.GetObject(key);
+
+ if (!hObject.IsArray) continue;
+
+ for (int x = 0; x < hObject.Length; x++)
+ {
+ switch (hObject.GetKeyTypeIndexed(x))
+ {
+ case Type_Int:
+ {
+ int temp = hObject.GetIntIndexed(x);
+ IntToString(temp, value, sizeof(value));
+ AppendToQueryString(paramCount, queryString, maxlength, key, value);
+ }
+ case Type_String:
+ {
+ hObject.GetStringIndexed(x, value, sizeof(value));
+ AppendToQueryString(paramCount, queryString, maxlength, key, value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ delete paramsMap;
+ }
+}
+
+// =====[ PRIVATE ]=====
+
+static void BoolToString(bool value, char[] buffer, int maxlength)
+{
+ FormatEx(buffer, maxlength, "%s", value ? "true" : "false");
+}
+
+static void AppendToQueryString(int &index, char[] buffer, int maxlength, char[] key, char[] value)
+{
+ if (index == 0)
+ {
+ index++;
+ Format(buffer, maxlength, "?%s=%s", key, value);
+ }
+ else
+ {
+ index++;
+ Format(buffer, maxlength, "%s&%s=%s", buffer, key, value);
+ }
+}
diff --git a/sourcemod/scripting/include/GlobalAPI/responses.inc b/sourcemod/scripting/include/GlobalAPI/responses.inc
new file mode 100644
index 0000000..62ebfd2
--- /dev/null
+++ b/sourcemod/scripting/include/GlobalAPI/responses.inc
@@ -0,0 +1,575 @@
+// ================== DOUBLE INCLUDE ========================= //
+
+#if defined _GlobalAPI_Responses_included_
+#endinput
+#endif
+#define _GlobalAPI_Responses_included_
+
+// =========================================================== //
+
+#include <json>
+#include <GlobalAPI/iterable>
+
+// =========================================================== //
+
+methodmap APIAuth < JSON_Object
+{
+ public APIAuth(JSON_Object hAuth)
+ {
+ return view_as<APIAuth>(hAuth);
+ }
+
+ public bool GetType(char[] buffer, int maxlength)
+ {
+ return this.GetString("Type", buffer, maxlength);
+ }
+
+ property bool IsValid
+ {
+ public get() { return this.GetBool("IsValid"); }
+ }
+
+ property int Identity
+ {
+ public get() { return this.GetInt("Identity"); }
+ }
+}
+
+// =========================================================== //
+
+methodmap APIBan < JSON_Object
+{
+ public APIBan(JSON_Object hBan)
+ {
+ return view_as<APIBan>(hBan);
+ }
+
+ property int Id
+ {
+ public get() { return this.GetInt("id"); }
+ }
+
+ property int UpdatedById
+ {
+ public get() { return this.GetInt("updated_by_id"); }
+ }
+
+ public void GetStats(char[] buffer, int maxlength)
+ {
+ this.GetString("stats", buffer, maxlength);
+ }
+
+ public void GetBanType(char[] buffer, int maxlength)
+ {
+ this.GetString("ban_type", buffer, maxlength);
+ }
+
+ public void GetExpiresOn(char[] buffer, int maxlength)
+ {
+ this.GetString("expires_on", buffer, maxlength);
+ }
+
+ public void GetSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("steamid64", buffer, maxlength);
+ }
+
+ public void GetPlayerName(char[] buffer, int maxlength)
+ {
+ this.GetString("player_name", buffer, maxlength);
+ }
+
+ public void GetNotes(char[] buffer, int maxlength)
+ {
+ this.GetString("notes", buffer, maxlength);
+ }
+
+ public void GetSteamId(char[] buffer, int maxlength)
+ {
+ this.GetString("steam_id", buffer, maxlength);
+ }
+
+ public void GetUpdatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_on", buffer, maxlength);
+ }
+
+ property int ServerId
+ {
+ public get() { return this.GetInt("server_id"); }
+ }
+
+ public void GetCreatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("created_on", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIJumpstat < JSON_Object
+{
+ public APIJumpstat(JSON_Object hJump)
+ {
+ return view_as<APIJumpstat>(hJump);
+ }
+
+ property int Id
+ {
+ public get() { return this.GetInt("id"); }
+ }
+
+ property int ServerId
+ {
+ public get() { return this.GetInt("server_id"); }
+ }
+
+ public void GetSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("steamid64", buffer, maxlength);
+ }
+
+ public void GetName(char[] buffer, int maxlength)
+ {
+ this.GetString("player_name", buffer, maxlength);
+ }
+
+ public void GetSteamId(char[] buffer, int maxlength)
+ {
+ this.GetString("steam_id", buffer, maxlength);
+ }
+
+ property int JumpType
+ {
+ public get() { return this.GetInt("jump_type"); }
+ }
+
+ property float Distance
+ {
+ public get() { return this.GetFloat("distance"); }
+ }
+
+ property int TickRate
+ {
+ public get() { return this.GetInt("tickrate"); }
+ }
+
+ property int MslCount
+ {
+ public get() { return this.GetInt("msl_count"); }
+ }
+
+ property int StrafeCount
+ {
+ public get() { return this.GetInt("strafe_count"); }
+ }
+
+ property bool IsCrouchBind
+ {
+ public get() { return this.GetBool("is_crouch_bind"); }
+ }
+
+ property bool IsForwardBind
+ {
+ public get() { return this.GetBool("is_forward_bind"); }
+ }
+
+ property bool IsCrouchBoost
+ {
+ public get() { return this.GetBool("is_crouch_boost"); }
+ }
+
+ property int UpdatedById
+ {
+ public get() { return this.GetInt("updated_by_id"); }
+ }
+
+ public void GetCreatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("created_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_on", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIMap < JSON_Object
+{
+ public APIMap(JSON_Object hMap)
+ {
+ return view_as<APIMap>(hMap);
+ }
+
+ property int Id
+ {
+ public get() { return this.GetInt("id"); }
+ }
+
+ public void GetName(char[] buffer, int maxlength)
+ {
+ this.GetString("name", buffer, maxlength);
+ }
+
+ property int Filesize
+ {
+ public get() { return this.GetInt("filesize"); }
+ }
+
+ property bool IsValidated
+ {
+ public get() { return this.GetBool("validated"); }
+ }
+
+ property int Difficulty
+ {
+ public get() { return this.GetInt("difficulty"); }
+ }
+
+ public void GetCreatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("created_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_on", buffer, maxlength);
+ }
+
+ public void GetApprovedBySteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("approved_by_steamid64", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIMode < JSON_Object
+{
+ public APIMode(JSON_Object hMode)
+ {
+ return view_as<APIMode>(hMode);
+ }
+
+ property int Id
+ {
+ public get() { return this.GetInt("id"); }
+ }
+
+ public void GetName(char[] buffer, int maxlength)
+ {
+ this.GetString("name", buffer, maxlength);
+ }
+
+ public void GetDescription(char[] buffer, int maxlength)
+ {
+ this.GetString("description", buffer, maxlength);
+ }
+
+ property int LatestVersion
+ {
+ public get() { return this.GetInt("latest_version"); }
+ }
+
+ public void GetLatestVersionDesc(char[] buffer, int maxlength)
+ {
+ this.GetString("latest_version_description", buffer, maxlength);
+ }
+
+ public void GetWebsite(char[] buffer, int maxlength)
+ {
+ this.GetString("website", buffer, maxlength);
+ }
+
+ public void GetRepository(char[] buffer, int maxlength)
+ {
+ this.GetString("repo", buffer, maxlength);
+ }
+
+ public void GetContactSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("contact_steamid64", buffer, maxlength);
+ }
+
+ // TODO: Add supported_tickrates
+
+ public void GetCreatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("created_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedById(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_by_id", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIPlayerRank < JSON_Object
+{
+ public APIPlayerRank(JSON_Object hPlayerRank)
+ {
+ return view_as<APIPlayerRank>(hPlayerRank);
+ }
+
+ property int Points
+ {
+ public get() { return this.GetInt("points"); }
+ }
+
+ property int PointsOverall
+ {
+ public get() { return this.GetInt("points_overall"); }
+ }
+
+ property float Average
+ {
+ public get() { return this.GetFloat("average"); }
+ }
+
+ property float Rating
+ {
+ public get() { return this.GetFloat("rating"); }
+ }
+
+ property int Finishes
+ {
+ public get() { return this.GetInt("finishes"); }
+ }
+
+ public void GetSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("steamid64", buffer, maxlength);
+ }
+
+ public void GetSteamId(char[] buffer, int maxlength)
+ {
+ this.GetString("steamid", buffer, maxlength);
+ }
+
+ public void GetPlayerName(char[] buffer, int maxlength)
+ {
+ this.GetString("player_name", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIPlayer < JSON_Object
+{
+ public APIPlayer(JSON_Object hPlayer)
+ {
+ return view_as<APIPlayer>(hPlayer);
+ }
+
+ public void GetSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("steamid64", buffer, maxlength);
+ }
+
+ public void GetSteamId(char[] buffer, int maxlength)
+ {
+ this.GetString("steam_id", buffer, maxlength);
+ }
+
+ property bool IsBanned
+ {
+ public get() { return this.GetBool("is_banned"); }
+ }
+
+ property int TotalRecords
+ {
+ public get() { return this.GetInt("total_records"); }
+ }
+
+ public void GetName(char[] buffer, int maxlength)
+ {
+ this.GetString("name", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIRecord < JSON_Object
+{
+ public APIRecord(JSON_Object hRecord)
+ {
+ return view_as<APIRecord>(hRecord);
+ }
+
+ property int Id
+ {
+ public get() { return this.GetInt("id"); }
+ }
+
+ public void GetSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("steamid64", buffer, maxlength);
+ }
+
+ public void GetPlayerName(char[] buffer, int maxlength)
+ {
+ this.GetString("player_name", buffer, maxlength);
+ }
+
+ public void GetSteamId(char[] buffer, int maxlength)
+ {
+ this.GetString("steam_id", buffer, maxlength);
+ }
+
+ property int ServerId
+ {
+ public get() { return this.GetInt("server_id"); }
+ }
+
+ property int MapId
+ {
+ public get() { return this.GetInt("map_id"); }
+ }
+
+ property int Stage
+ {
+ public get() { return this.GetInt("stage"); }
+ }
+
+ public void GetMode(char[] buffer, int maxlength)
+ {
+ this.GetString("mode", buffer, maxlength);
+ }
+
+ property int TickRate
+ {
+ public get() { return this.GetInt("tickrate"); }
+ }
+
+ property float Time
+ {
+ public get() { return this.GetFloat("time"); }
+ }
+
+ property int Teleports
+ {
+ public get() { return this.GetInt("teleports"); }
+ }
+
+ public void GetCreatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("created_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_on", buffer, maxlength);
+ }
+
+ property int UpdatedBy
+ {
+ public get() { return this.GetInt("updated_by"); }
+ }
+
+ public void GetServerName(char[] buffer, int maxlength)
+ {
+ this.GetString("server_name", buffer, maxlength);
+ }
+
+ public void GetMapName(char[] buffer, int maxlength)
+ {
+ this.GetString("map_name", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIServer < JSON_Object
+{
+ public APIServer(JSON_Object hServer)
+ {
+ return view_as<APIServer>(hServer);
+ }
+
+ property int Port
+ {
+ public get() { return this.GetInt("port"); }
+ }
+
+ public void GetIPAddress(char[] buffer, int maxlength)
+ {
+ this.GetString("ip", buffer, maxlength);
+ }
+
+ public void GetName(char[] buffer, int maxlength)
+ {
+ this.GetString("name", buffer, maxlength);
+ }
+
+ public void GetOwnerSteamId64(char[] buffer, int maxlength)
+ {
+ this.GetString("owner_steamid64", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
+
+methodmap APIRecordFilter < JSON_Object
+{
+ public APIRecordFilter(JSON_Object hRecordFilter)
+ {
+ return view_as<APIRecordFilter>(hRecordFilter);
+ }
+
+ property int Id
+ {
+ public get() { return this.GetInt("id"); }
+ }
+
+ property int MapId
+ {
+ public get() { return this.GetInt("map_id"); }
+ }
+
+ property int Stage
+ {
+ public get() { return this.GetInt("stage"); }
+ }
+
+ property int ModeId
+ {
+ public get() { return this.GetInt("mode_id"); }
+ }
+
+ property int TickRate
+ {
+ public get() { return this.GetInt("tickrate"); }
+ }
+
+ property bool HasTeleports
+ {
+ public get() { return this.GetBool("has_teleports"); }
+ }
+
+ public void GetCreatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("created_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedOn(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_on", buffer, maxlength);
+ }
+
+ public void GetUpdatedById(char[] buffer, int maxlength)
+ {
+ this.GetString("updated_by_id", buffer, maxlength);
+ }
+}
+
+// =========================================================== //
diff --git a/sourcemod/scripting/include/GlobalAPI/stocks.inc b/sourcemod/scripting/include/GlobalAPI/stocks.inc
new file mode 100644
index 0000000..52596c4
--- /dev/null
+++ b/sourcemod/scripting/include/GlobalAPI/stocks.inc
@@ -0,0 +1,67 @@
+// ================== DOUBLE INCLUDE ========================= //
+
+#if defined _GlobalAPI_Stocks_included_
+#endinput
+#endif
+#define _GlobalAPI_Stocks_included_
+
+// =========================================================== //
+
+/**
+ * Gets plugin's display name from its handle
+ *
+ * @param plugin Plugin handle to retrieve name from
+ * @return String representation of the plugin name
+ */
+stock char[] GetPluginDisplayName(Handle plugin)
+{
+ char pluginName[GlobalAPI_Max_PluginName_Length] = "Unknown";
+ GetPluginInfo(plugin, PlInfo_Name, pluginName, sizeof(pluginName));
+
+ return pluginName;
+}
+
+/**
+ * Gets plugin's version from its handle
+ *
+ * @param plugin Plugin handle to retrieve version from
+ * @return String representation of the plugin version
+ */
+stock char[] GetPluginVersion(Handle plugin)
+{
+ char pluginVersion[GlobalAPI_Max_PluginVersion_Length] = "Unknown";
+ GetPluginInfo(plugin, PlInfo_Version, pluginVersion, sizeof(pluginVersion));
+
+ return pluginVersion;
+}
+
+/**
+ * Gets current map's "display name"
+ *
+ * @param buffer Buffer to store the result in
+ * @param maxlength Max length of the buffer
+ * @noreturn
+ */
+stock void GetMapDisplay(char[] buffer, int maxlength)
+{
+ char map[PLATFORM_MAX_PATH];
+ GetCurrentMap(map, sizeof(map));
+ GetMapDisplayName(map, map, sizeof(map));
+
+ FormatEx(buffer, maxlength, map);
+}
+
+/**
+ * Gets current map's full (game dir) path
+ *
+ * @param buffer Buffer to store result in
+ * @param maxlength Max length of the buffer
+ * @noreturn
+ */
+stock void GetMapFullPath(char[] buffer, int maxlength)
+{
+ char mapPath[PLATFORM_MAX_PATH];
+ GetCurrentMap(mapPath, sizeof(mapPath));
+
+ Format(buffer, maxlength, "maps/%s.bsp", mapPath);
+}
diff --git a/sourcemod/scripting/include/SteamWorks.inc b/sourcemod/scripting/include/SteamWorks.inc
new file mode 100644
index 0000000..0e4aec3
--- /dev/null
+++ b/sourcemod/scripting/include/SteamWorks.inc
@@ -0,0 +1,413 @@
+#if defined _SteamWorks_Included
+ #endinput
+#endif
+#define _SteamWorks_Included
+
+/* results from UserHasLicenseForApp */
+enum EUserHasLicenseForAppResult
+{
+ k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app
+ k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app
+ k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated
+};
+
+/* General result codes */
+enum EResult
+{
+ k_EResultOK = 1, // success
+ k_EResultFail = 2, // generic failure
+ k_EResultNoConnection = 3, // no/failed network connection
+// k_EResultNoConnectionRetry = 4, // OBSOLETE - removed
+ k_EResultInvalidPassword = 5, // password/ticket is invalid
+ k_EResultLoggedInElsewhere = 6, // same user logged in elsewhere
+ k_EResultInvalidProtocolVer = 7, // protocol version is incorrect
+ k_EResultInvalidParam = 8, // a parameter is incorrect
+ k_EResultFileNotFound = 9, // file was not found
+ k_EResultBusy = 10, // called method busy - action not taken
+ k_EResultInvalidState = 11, // called object was in an invalid state
+ k_EResultInvalidName = 12, // name is invalid
+ k_EResultInvalidEmail = 13, // email is invalid
+ k_EResultDuplicateName = 14, // name is not unique
+ k_EResultAccessDenied = 15, // access is denied
+ k_EResultTimeout = 16, // operation timed out
+ k_EResultBanned = 17, // VAC2 banned
+ k_EResultAccountNotFound = 18, // account not found
+ k_EResultInvalidSteamID = 19, // steamID is invalid
+ k_EResultServiceUnavailable = 20, // The requested service is currently unavailable
+ k_EResultNotLoggedOn = 21, // The user is not logged on
+ k_EResultPending = 22, // Request is pending (may be in process, or waiting on third party)
+ k_EResultEncryptionFailure = 23, // Encryption or Decryption failed
+ k_EResultInsufficientPrivilege = 24, // Insufficient privilege
+ k_EResultLimitExceeded = 25, // Too much of a good thing
+ k_EResultRevoked = 26, // Access has been revoked (used for revoked guest passes)
+ k_EResultExpired = 27, // License/Guest pass the user is trying to access is expired
+ k_EResultAlreadyRedeemed = 28, // Guest pass has already been redeemed by account, cannot be acked again
+ k_EResultDuplicateRequest = 29, // The request is a duplicate and the action has already occurred in the past, ignored this time
+ k_EResultAlreadyOwned = 30, // All the games in this guest pass redemption request are already owned by the user
+ k_EResultIPNotFound = 31, // IP address not found
+ k_EResultPersistFailed = 32, // failed to write change to the data store
+ k_EResultLockingFailed = 33, // failed to acquire access lock for this operation
+ k_EResultLogonSessionReplaced = 34,
+ k_EResultConnectFailed = 35,
+ k_EResultHandshakeFailed = 36,
+ k_EResultIOFailure = 37,
+ k_EResultRemoteDisconnect = 38,
+ k_EResultShoppingCartNotFound = 39, // failed to find the shopping cart requested
+ k_EResultBlocked = 40, // a user didn't allow it
+ k_EResultIgnored = 41, // target is ignoring sender
+ k_EResultNoMatch = 42, // nothing matching the request found
+ k_EResultAccountDisabled = 43,
+ k_EResultServiceReadOnly = 44, // this service is not accepting content changes right now
+ k_EResultAccountNotFeatured = 45, // account doesn't have value, so this feature isn't available
+ k_EResultAdministratorOK = 46, // allowed to take this action, but only because requester is admin
+ k_EResultContentVersion = 47, // A Version mismatch in content transmitted within the Steam protocol.
+ k_EResultTryAnotherCM = 48, // The current CM can't service the user making a request, user should try another.
+ k_EResultPasswordRequiredToKickSession = 49,// You are already logged in elsewhere, this cached credential login has failed.
+ k_EResultAlreadyLoggedInElsewhere = 50, // You are already logged in elsewhere, you must wait
+ k_EResultSuspended = 51, // Long running operation (content download) suspended/paused
+ k_EResultCancelled = 52, // Operation canceled (typically by user: content download)
+ k_EResultDataCorruption = 53, // Operation canceled because data is ill formed or unrecoverable
+ k_EResultDiskFull = 54, // Operation canceled - not enough disk space.
+ k_EResultRemoteCallFailed = 55, // an remote call or IPC call failed
+ k_EResultPasswordUnset = 56, // Password could not be verified as it's unset server side
+ k_EResultExternalAccountUnlinked = 57, // External account (PSN, Facebook...) is not linked to a Steam account
+ k_EResultPSNTicketInvalid = 58, // PSN ticket was invalid
+ k_EResultExternalAccountAlreadyLinked = 59, // External account (PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first
+ k_EResultRemoteFileConflict = 60, // The sync cannot resume due to a conflict between the local and remote files
+ k_EResultIllegalPassword = 61, // The requested new password is not legal
+ k_EResultSameAsPreviousValue = 62, // new value is the same as the old one ( secret question and answer )
+ k_EResultAccountLogonDenied = 63, // account login denied due to 2nd factor authentication failure
+ k_EResultCannotUseOldPassword = 64, // The requested new password is not legal
+ k_EResultInvalidLoginAuthCode = 65, // account login denied due to auth code invalid
+ k_EResultAccountLogonDeniedNoMail = 66, // account login denied due to 2nd factor auth failure - and no mail has been sent
+ k_EResultHardwareNotCapableOfIPT = 67, //
+ k_EResultIPTInitError = 68, //
+ k_EResultParentalControlRestricted = 69, // operation failed due to parental control restrictions for current user
+ k_EResultFacebookQueryError = 70, // Facebook query returned an error
+ k_EResultExpiredLoginAuthCode = 71, // account login denied due to auth code expired
+ k_EResultIPLoginRestrictionFailed = 72,
+ k_EResultAccountLockedDown = 73,
+ k_EResultAccountLogonDeniedVerifiedEmailRequired = 74,
+ k_EResultNoMatchingURL = 75,
+ k_EResultBadResponse = 76, // parse failure, missing field, etc.
+ k_EResultRequirePasswordReEntry = 77, // The user cannot complete the action until they re-enter their password
+ k_EResultValueOutOfRange = 78, // the value entered is outside the acceptable range
+ k_EResultUnexpectedError = 79, // something happened that we didn't expect to ever happen
+ k_EResultDisabled = 80, // The requested service has been configured to be unavailable
+ k_EResultInvalidCEGSubmission = 81, // The set of files submitted to the CEG server are not valid !
+ k_EResultRestrictedDevice = 82, // The device being used is not allowed to perform this action
+ k_EResultRegionLocked = 83, // The action could not be complete because it is region restricted
+ k_EResultRateLimitExceeded = 84, // Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent
+ k_EResultAccountLoginDeniedNeedTwoFactor = 85, // Need two-factor code to login
+ k_EResultItemDeleted = 86, // The thing we're trying to access has been deleted
+ k_EResultAccountLoginDeniedThrottle = 87, // login attempt failed, try to throttle response to possible attacker
+ k_EResultTwoFactorCodeMismatch = 88, // two factor code mismatch
+ k_EResultTwoFactorActivationCodeMismatch = 89, // activation code for two-factor didn't match
+ k_EResultAccountAssociatedToMultiplePartners = 90, // account has been associated with multiple partners
+ k_EResultNotModified = 91, // data not modified
+ k_EResultNoMobileDevice = 92, // the account does not have a mobile device associated with it
+ k_EResultTimeNotSynced = 93, // the time presented is out of range or tolerance
+ k_EResultSmsCodeFailed = 94, // SMS code failure (no match, none pending, etc.)
+ k_EResultAccountLimitExceeded = 95, // Too many accounts access this resource
+ k_EResultAccountActivityLimitExceeded = 96, // Too many changes to this account
+ k_EResultPhoneActivityLimitExceeded = 97, // Too many changes to this phone
+ k_EResultRefundToWallet = 98, // Cannot refund to payment method, must use wallet
+ k_EResultEmailSendFailure = 99, // Cannot send an email
+ k_EResultNotSettled = 100, // Can't perform operation till payment has settled
+ k_EResultNeedCaptcha = 101, // Needs to provide a valid captcha
+ k_EResultGSLTDenied = 102, // a game server login token owned by this token's owner has been banned
+ k_EResultGSOwnerDenied = 103, // game server owner is denied for other reason (account lock, community ban, vac ban, missing phone)
+ k_EResultInvalidItemType = 104 // the type of thing we were requested to act on is invalid
+};
+
+/* This enum is used in client API methods, do not re-number existing values. */
+enum EHTTPMethod
+{
+ k_EHTTPMethodInvalid = 0,
+ k_EHTTPMethodGET,
+ k_EHTTPMethodHEAD,
+ k_EHTTPMethodPOST,
+ k_EHTTPMethodPUT,
+ k_EHTTPMethodDELETE,
+ k_EHTTPMethodOPTIONS,
+ k_EHTTPMethodPATCH,
+
+ // The remaining HTTP methods are not yet supported, per rfc2616 section 5.1.1 only GET and HEAD are required for
+ // a compliant general purpose server. We'll likely add more as we find uses for them.
+
+ // k_EHTTPMethodTRACE,
+ // k_EHTTPMethodCONNECT
+};
+
+
+/* HTTP Status codes that the server can send in response to a request, see rfc2616 section 10.3 for descriptions
+ of each of these. */
+enum EHTTPStatusCode
+{
+ // Invalid status code (this isn't defined in HTTP, used to indicate unset in our code)
+ k_EHTTPStatusCodeInvalid = 0,
+
+ // Informational codes
+ k_EHTTPStatusCode100Continue = 100,
+ k_EHTTPStatusCode101SwitchingProtocols = 101,
+
+ // Success codes
+ k_EHTTPStatusCode200OK = 200,
+ k_EHTTPStatusCode201Created = 201,
+ k_EHTTPStatusCode202Accepted = 202,
+ k_EHTTPStatusCode203NonAuthoritative = 203,
+ k_EHTTPStatusCode204NoContent = 204,
+ k_EHTTPStatusCode205ResetContent = 205,
+ k_EHTTPStatusCode206PartialContent = 206,
+
+ // Redirection codes
+ k_EHTTPStatusCode300MultipleChoices = 300,
+ k_EHTTPStatusCode301MovedPermanently = 301,
+ k_EHTTPStatusCode302Found = 302,
+ k_EHTTPStatusCode303SeeOther = 303,
+ k_EHTTPStatusCode304NotModified = 304,
+ k_EHTTPStatusCode305UseProxy = 305,
+ //k_EHTTPStatusCode306Unused = 306, (used in old HTTP spec, now unused in 1.1)
+ k_EHTTPStatusCode307TemporaryRedirect = 307,
+
+ // Error codes
+ k_EHTTPStatusCode400BadRequest = 400,
+ k_EHTTPStatusCode401Unauthorized = 401, // You probably want 403 or something else. 401 implies you're sending a WWW-Authenticate header and the client can sent an Authorization header in response.
+ k_EHTTPStatusCode402PaymentRequired = 402, // This is reserved for future HTTP specs, not really supported by clients
+ k_EHTTPStatusCode403Forbidden = 403,
+ k_EHTTPStatusCode404NotFound = 404,
+ k_EHTTPStatusCode405MethodNotAllowed = 405,
+ k_EHTTPStatusCode406NotAcceptable = 406,
+ k_EHTTPStatusCode407ProxyAuthRequired = 407,
+ k_EHTTPStatusCode408RequestTimeout = 408,
+ k_EHTTPStatusCode409Conflict = 409,
+ k_EHTTPStatusCode410Gone = 410,
+ k_EHTTPStatusCode411LengthRequired = 411,
+ k_EHTTPStatusCode412PreconditionFailed = 412,
+ k_EHTTPStatusCode413RequestEntityTooLarge = 413,
+ k_EHTTPStatusCode414RequestURITooLong = 414,
+ k_EHTTPStatusCode415UnsupportedMediaType = 415,
+ k_EHTTPStatusCode416RequestedRangeNotSatisfiable = 416,
+ k_EHTTPStatusCode417ExpectationFailed = 417,
+ k_EHTTPStatusCode4xxUnknown = 418, // 418 is reserved, so we'll use it to mean unknown
+ k_EHTTPStatusCode429TooManyRequests = 429,
+
+ // Server error codes
+ k_EHTTPStatusCode500InternalServerError = 500,
+ k_EHTTPStatusCode501NotImplemented = 501,
+ k_EHTTPStatusCode502BadGateway = 502,
+ k_EHTTPStatusCode503ServiceUnavailable = 503,
+ k_EHTTPStatusCode504GatewayTimeout = 504,
+ k_EHTTPStatusCode505HTTPVersionNotSupported = 505,
+ k_EHTTPStatusCode5xxUnknown = 599,
+};
+
+/* list of possible return values from the ISteamGameCoordinator API */
+enum EGCResults
+{
+ k_EGCResultOK = 0,
+ k_EGCResultNoMessage = 1, // There is no message in the queue
+ k_EGCResultBufferTooSmall = 2, // The buffer is too small for the requested message
+ k_EGCResultNotLoggedOn = 3, // The client is not logged onto Steam
+ k_EGCResultInvalidMessage = 4, // Something was wrong with the message being sent with SendMessage
+};
+
+native bool:SteamWorks_IsVACEnabled();
+native bool:SteamWorks_GetPublicIP(ipaddr[4]);
+native SteamWorks_GetPublicIPCell();
+native bool:SteamWorks_IsLoaded();
+native bool:SteamWorks_SetGameData(const String:sData[]);
+native bool:SteamWorks_SetGameDescription(const String:sDesc[]);
+native bool:SteamWorks_SetMapName(const String:sMapName[]);
+native bool:SteamWorks_IsConnected();
+native bool:SteamWorks_SetRule(const String:sKey[], const String:sValue[]);
+native bool:SteamWorks_ClearRules();
+native bool:SteamWorks_ForceHeartbeat();
+native bool:SteamWorks_GetUserGroupStatus(client, groupid);
+native bool:SteamWorks_GetUserGroupStatusAuthID(authid, groupid);
+
+native EUserHasLicenseForAppResult:SteamWorks_HasLicenseForApp(client, app);
+native EUserHasLicenseForAppResult:SteamWorks_HasLicenseForAppId(authid, app);
+native SteamWorks_GetClientSteamID(client, String:sSteamID[], length);
+
+native bool:SteamWorks_RequestStatsAuthID(authid, appid);
+native bool:SteamWorks_RequestStats(client, appid);
+native bool:SteamWorks_GetStatCell(client, const String:sKey[], &value);
+native bool:SteamWorks_GetStatAuthIDCell(authid, const String:sKey[], &value);
+native bool:SteamWorks_GetStatFloat(client, const String:sKey[], &Float:value);
+native bool:SteamWorks_GetStatAuthIDFloat(authid, const String:sKey[], &Float:value);
+
+native Handle:SteamWorks_CreateHTTPRequest(EHTTPMethod:method, const String:sURL[]);
+native bool:SteamWorks_SetHTTPRequestContextValue(Handle:hHandle, any:data1, any:data2=0);
+native bool:SteamWorks_SetHTTPRequestNetworkActivityTimeout(Handle:hHandle, timeout);
+native bool:SteamWorks_SetHTTPRequestHeaderValue(Handle:hHandle, const String:sName[], const String:sValue[]);
+native bool:SteamWorks_SetHTTPRequestGetOrPostParameter(Handle:hHandle, const String:sName[], const String:sValue[]);
+native bool:SteamWorks_SetHTTPRequestUserAgentInfo(Handle:hHandle, const String:sUserAgentInfo[]);
+native bool:SteamWorks_SetHTTPRequestRequiresVerifiedCertificate(Handle:hHandle, bool:bRequireVerifiedCertificate);
+native bool:SteamWorks_SetHTTPRequestAbsoluteTimeoutMS(Handle:hHandle, unMilliseconds);
+
+#if SOURCEMOD_V_MAJOR >= 1 && SOURCEMOD_V_MINOR >= 9
+typeset SteamWorksHTTPRequestCompleted
+{
+ function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode);
+ function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1);
+ function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1, any data2);
+};
+
+typeset SteamWorksHTTPHeadersReceived
+{
+ function void (Handle hRequest, bool bFailure);
+ function void (Handle hRequest, bool bFailure, any data1);
+ function void (Handle hRequest, bool bFailure, any data1, any data2);
+};
+
+typeset SteamWorksHTTPDataReceived
+{
+ function void (Handle hRequest, bool bFailure, int offset, int bytesreceived);
+ function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1);
+ function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1, any data2);
+};
+
+typeset SteamWorksHTTPBodyCallback
+{
+ function void (const char[] sData);
+ function void (const char[] sData, any value);
+ function void (const int[] data, any value, int datalen);
+};
+
+#else
+
+funcenum SteamWorksHTTPRequestCompleted
+{
+ public(Handle:hRequest, bool:bFailure, bool:bRequestSuccessful, EHTTPStatusCode:eStatusCode),
+ public(Handle:hRequest, bool:bFailure, bool:bRequestSuccessful, EHTTPStatusCode:eStatusCode, any:data1),
+ public(Handle:hRequest, bool:bFailure, bool:bRequestSuccessful, EHTTPStatusCode:eStatusCode, any:data1, any:data2)
+};
+
+funcenum SteamWorksHTTPHeadersReceived
+{
+ public(Handle:hRequest, bool:bFailure),
+ public(Handle:hRequest, bool:bFailure, any:data1),
+ public(Handle:hRequest, bool:bFailure, any:data1, any:data2)
+};
+
+funcenum SteamWorksHTTPDataReceived
+{
+ public(Handle:hRequest, bool:bFailure, offset, bytesreceived),
+ public(Handle:hRequest, bool:bFailure, offset, bytesreceived, any:data1),
+ public(Handle:hRequest, bool:bFailure, offset, bytesreceived, any:data1, any:data2)
+};
+
+funcenum SteamWorksHTTPBodyCallback
+{
+ public(const String:sData[]),
+ public(const String:sData[], any:value),
+ public(const data[], any:value, datalen)
+};
+
+#endif
+
+native bool:SteamWorks_SetHTTPCallbacks(Handle:hHandle, SteamWorksHTTPRequestCompleted:fCompleted = INVALID_FUNCTION, SteamWorksHTTPHeadersReceived:fHeaders = INVALID_FUNCTION, SteamWorksHTTPDataReceived:fData = INVALID_FUNCTION, Handle:hCalling = INVALID_HANDLE);
+native bool:SteamWorks_SendHTTPRequest(Handle:hRequest);
+native bool:SteamWorks_SendHTTPRequestAndStreamResponse(Handle:hRequest);
+native bool:SteamWorks_DeferHTTPRequest(Handle:hRequest);
+native bool:SteamWorks_PrioritizeHTTPRequest(Handle:hRequest);
+native bool:SteamWorks_GetHTTPResponseHeaderSize(Handle:hRequest, const String:sHeader[], &size);
+native bool:SteamWorks_GetHTTPResponseHeaderValue(Handle:hRequest, const String:sHeader[], String:sValue[], size);
+native bool:SteamWorks_GetHTTPResponseBodySize(Handle:hRequest, &size);
+native bool:SteamWorks_GetHTTPResponseBodyData(Handle:hRequest, String:sBody[], length);
+native bool:SteamWorks_GetHTTPStreamingResponseBodyData(Handle:hRequest, cOffset, String:sBody[], length);
+native bool:SteamWorks_GetHTTPDownloadProgressPct(Handle:hRequest, &Float:percent);
+native bool:SteamWorks_GetHTTPRequestWasTimedOut(Handle:hRequest, &bool:bWasTimedOut);
+native bool:SteamWorks_SetHTTPRequestRawPostBody(Handle:hRequest, const String:sContentType[], const String:sBody[], bodylen);
+native bool:SteamWorks_SetHTTPRequestRawPostBodyFromFile(Handle:hRequest, const String:sContentType[], const String:sFileName[]);
+
+native bool:SteamWorks_GetHTTPResponseBodyCallback(Handle:hRequest, SteamWorksHTTPBodyCallback:fCallback, any:data = 0, Handle:hPlugin = INVALID_HANDLE); /* Look up, moved definition for 1.7+ compat. */
+native bool:SteamWorks_WriteHTTPResponseBodyToFile(Handle:hRequest, const String:sFileName[]);
+
+forward SW_OnValidateClient(ownerauthid, authid);
+forward SteamWorks_OnValidateClient(ownerauthid, authid);
+forward SteamWorks_SteamServersConnected();
+forward SteamWorks_SteamServersConnectFailure(EResult:result);
+forward SteamWorks_SteamServersDisconnected(EResult:result);
+
+forward Action:SteamWorks_RestartRequested();
+forward SteamWorks_TokenRequested(String:sToken[], maxlen);
+
+forward SteamWorks_OnClientGroupStatus(authid, groupid, bool:isMember, bool:isOfficer);
+
+forward EGCResults:SteamWorks_GCSendMessage(unMsgType, const String:pubData[], cubData);
+forward SteamWorks_GCMsgAvailable(cubData);
+forward EGCResults:SteamWorks_GCRetrieveMessage(punMsgType, const String:pubDest[], cubDest, pcubMsgSize);
+
+native EGCResults:SteamWorks_SendMessageToGC(unMsgType, const String:pubData[], cubData);
+
+public Extension:__ext_SteamWorks =
+{
+ name = "SteamWorks",
+ file = "SteamWorks.ext",
+#if defined AUTOLOAD_EXTENSIONS
+ autoload = 1,
+#else
+ autoload = 0,
+#endif
+#if defined REQUIRE_EXTENSIONS
+ required = 1,
+#else
+ required = 0,
+#endif
+};
+
+#if !defined REQUIRE_EXTENSIONS
+public __ext_SteamWorks_SetNTVOptional()
+{
+ MarkNativeAsOptional("SteamWorks_IsVACEnabled");
+ MarkNativeAsOptional("SteamWorks_GetPublicIP");
+ MarkNativeAsOptional("SteamWorks_GetPublicIPCell");
+ MarkNativeAsOptional("SteamWorks_IsLoaded");
+ MarkNativeAsOptional("SteamWorks_SetGameData");
+ MarkNativeAsOptional("SteamWorks_SetGameDescription");
+ MarkNativeAsOptional("SteamWorks_IsConnected");
+ MarkNativeAsOptional("SteamWorks_SetRule");
+ MarkNativeAsOptional("SteamWorks_ClearRules");
+ MarkNativeAsOptional("SteamWorks_ForceHeartbeat");
+ MarkNativeAsOptional("SteamWorks_GetUserGroupStatus");
+ MarkNativeAsOptional("SteamWorks_GetUserGroupStatusAuthID");
+
+ MarkNativeAsOptional("SteamWorks_HasLicenseForApp");
+ MarkNativeAsOptional("SteamWorks_HasLicenseForAppId");
+ MarkNativeAsOptional("SteamWorks_GetClientSteamID");
+
+ MarkNativeAsOptional("SteamWorks_RequestStatsAuthID");
+ MarkNativeAsOptional("SteamWorks_RequestStats");
+ MarkNativeAsOptional("SteamWorks_GetStatCell");
+ MarkNativeAsOptional("SteamWorks_GetStatAuthIDCell");
+ MarkNativeAsOptional("SteamWorks_GetStatFloat");
+ MarkNativeAsOptional("SteamWorks_GetStatAuthIDFloat");
+
+ MarkNativeAsOptional("SteamWorks_SendMessageToGC");
+
+ MarkNativeAsOptional("SteamWorks_CreateHTTPRequest");
+ MarkNativeAsOptional("SteamWorks_SetHTTPRequestContextValue");
+ MarkNativeAsOptional("SteamWorks_SetHTTPRequestNetworkActivityTimeout");
+ MarkNativeAsOptional("SteamWorks_SetHTTPRequestHeaderValue");
+ MarkNativeAsOptional("SteamWorks_SetHTTPRequestGetOrPostParameter");
+
+ MarkNativeAsOptional("SteamWorks_SetHTTPCallbacks");
+ MarkNativeAsOptional("SteamWorks_SendHTTPRequest");
+ MarkNativeAsOptional("SteamWorks_SendHTTPRequestAndStreamResponse");
+ MarkNativeAsOptional("SteamWorks_DeferHTTPRequest");
+ MarkNativeAsOptional("SteamWorks_PrioritizeHTTPRequest");
+ MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderSize");
+ MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderValue");
+ MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodySize");
+ MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyData");
+ MarkNativeAsOptional("SteamWorks_GetHTTPStreamingResponseBodyData");
+ MarkNativeAsOptional("SteamWorks_GetHTTPDownloadProgressPct");
+ MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBody");
+ MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBodyFromFile");
+
+ MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyCallback");
+ MarkNativeAsOptional("SteamWorks_WriteHTTPResponseBodyToFile");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/autoexecconfig.inc b/sourcemod/scripting/include/autoexecconfig.inc
new file mode 100644
index 0000000..e057b1b
--- /dev/null
+++ b/sourcemod/scripting/include/autoexecconfig.inc
@@ -0,0 +1,765 @@
+/**
+ * AutoExecConfig
+ *
+ * Copyright (C) 2013-2017 Impact
+ *
+ * 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
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+#if defined _autoexecconfig_included
+ #endinput
+#endif
+#define _autoexecconfig_included
+
+
+#include <sourcemod>
+
+#define AUTOEXECCONFIG_VERSION "0.1.5"
+#define AUTOEXECCONFIG_URL "https://forums.alliedmods.net/showthread.php?t=204254"
+
+// Append
+#define AUTOEXEC_APPEND_BAD_FILENAME 0
+#define AUTOEXEC_APPEND_FILE_NOT_FOUND 1
+#define AUTOEXEC_APPEND_BAD_HANDLE 2
+#define AUTOEXEC_APPEND_SUCCESS 3
+
+
+
+// Find
+#define AUTOEXEC_FIND_BAD_FILENAME 10
+#define AUTOEXEC_FIND_FILE_NOT_FOUND 11
+#define AUTOEXEC_FIND_BAD_HANDLE 12
+#define AUTOEXEC_FIND_NOT_FOUND 13
+#define AUTOEXEC_FIND_SUCCESS 14
+
+
+
+// Clean
+#define AUTOEXEC_CLEAN_FILE_NOT_FOUND 20
+#define AUTOEXEC_CLEAN_BAD_HANDLE 21
+#define AUTOEXEC_CLEAN_SUCCESS 22
+
+
+
+// General
+#define AUTOEXEC_NO_CONFIG 30
+
+
+
+// Formatter
+#define AUTOEXEC_FORMAT_BAD_FILENAME 40
+#define AUTOEXEC_FORMAT_SUCCESS 41
+
+
+
+// Global variables
+static char g_sConfigFile[PLATFORM_MAX_PATH];
+static char g_sRawFileName[PLATFORM_MAX_PATH];
+static char g_sFolderPath[PLATFORM_MAX_PATH];
+
+static bool g_bCreateFile = false;
+static Handle g_hPluginHandle = null;
+
+static bool g_bCreateDirectory = false;
+static int g_bCreateDirectoryMode = FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC;
+
+
+// Workaround for now
+static int g_iLastFindResult;
+static int g_iLastAppendResult;
+
+
+
+
+/**
+ * Returns the last result from the parser.
+ *
+ * @return Returns one of the AUTOEXEC_FIND values or -1 if not set.
+*/
+stock int AutoExecConfig_GetFindResult()
+{
+ return g_iLastFindResult;
+}
+
+
+
+
+
+/**
+ * Returns the last result from the appender.
+ *
+ * @return Returns one of the AUTOEXEC_APPEND values or -1 if not set.
+*/
+stock int AutoExecConfig_GetAppendResult()
+{
+ return g_iLastAppendResult;
+}
+
+
+/**
+ * Set if the config file should be created by the autoexecconfig include itself if it doesn't exist.
+ *
+ * @param create True if config file should be created, false otherwise.
+ * @noreturn
+ */
+stock void AutoExecConfig_SetCreateFile(bool create)
+{
+ g_bCreateFile = create;
+}
+
+
+/**
+ * Set if the config file's folder should be created by the autoexecconfig include itself if it doesn't exist.
+ * Note: Must be used before AutoExecConfig_SetFile as the potential creation of it happens there
+ *
+ * @param create True if config file should be created, false otherwise.
+ * @param mode Folder permission mode, default is u=rwx,g=rx,o=rx.
+ * @noreturn
+ */
+stock void AutoExecConfig_SetCreateDirectory(bool create, int mode=FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC)
+{
+ g_bCreateDirectory = create;
+ g_bCreateDirectoryMode = mode;
+}
+
+
+/**
+ * Returns if the config file should be created if it doesn't exist.
+ *
+ * @return Returns true, if the config file should be created or false if it should not.
+ */
+stock bool AutoExecConfig_GetCreateFile()
+{
+ return g_bCreateFile;
+}
+
+
+/**
+ * Set the plugin for which the config file should be created.
+ * Set to null to use the calling plugin.
+ * Used to print the correct filename in the top comment when creating the file.
+ *
+ * @param plugin The plugin to create convars for or null to use the calling plugin.
+ * @noreturn
+ */
+stock void AutoExecConfig_SetPlugin(Handle plugin)
+{
+ g_hPluginHandle = plugin;
+}
+
+
+/**
+ * Returns the plugin's handle for which the config file is created.
+ *
+ * @return The plugin handle
+ */
+stock Handle AutoExecConfig_GetPlugin()
+{
+ return g_hPluginHandle;
+}
+
+
+/**
+ * Set the global autoconfigfile used by functions of this file.
+ * Note: does not support subfolders like folder1/folder2
+ *
+ * @param file Name of the config file, path and .cfg extension is being added if not given.
+ * @param folder Folder under cfg/ to use. By default this is "sourcemod."
+ * @return True if formatter returned success, false otherwise.
+*/
+stock bool AutoExecConfig_SetFile(char[] file, char[] folder="sourcemod")
+{
+ Format(g_sConfigFile, sizeof(g_sConfigFile), "%s", file);
+
+ // Global buffers for cfg execution
+ strcopy(g_sRawFileName, sizeof(g_sRawFileName), file);
+ strcopy(g_sFolderPath, sizeof(g_sFolderPath), folder);
+
+
+ // Format the filename
+ return AutoExecConfig_FormatFileName(g_sConfigFile, sizeof(g_sConfigFile), folder) == AUTOEXEC_FORMAT_SUCCESS;
+}
+
+
+
+
+
+
+/**
+ * Get the formatted autoconfigfile used by functions of this file.
+ *
+ * @param buffer String to format.
+ * @param size Maximum size of buffer
+ * @return True if filename was set, false otherwise.
+*/
+stock bool AutoExecConfig_GetFile(char[] buffer,int size)
+{
+ if (strlen(g_sConfigFile) > 0)
+ {
+ strcopy(buffer, size, g_sConfigFile);
+
+ return true;
+ }
+
+ // Security for decl users
+ buffer[0] = '\0';
+
+ return false;
+}
+
+
+
+
+
+
+/**
+ * Creates a convar and appends it to the autoconfigfile if not found.
+ * FCVAR_DONTRECORD will be skipped.
+ *
+ * @param name Name of new convar.
+ * @param defaultValue String containing the default value of new convar.
+ * @param description Optional description of the convar.
+ * @param flags Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details.
+ * @param hasMin Optional boolean that determines if the convar has a minimum value.
+ * @param min Minimum floating point value that the convar can have if hasMin is true.
+ * @param hasMax Optional boolean that determines if the convar has a maximum value.
+ * @param max Maximum floating point value that the convar can have if hasMax is true.
+ * @return A handle to the newly created convar. If the convar already exists, a handle to it will still be returned.
+ * @error Convar name is blank or is the same as an existing console command.
+*/
+stock ConVar AutoExecConfig_CreateConVar(const char[] name, const char[] defaultValue, const char[] description="", int flags=0, bool hasMin=false, float min=0.0, bool hasMax=false, float max=0.0)
+{
+ // If configfile was set and convar has no dontrecord flag
+ if (!(flags & FCVAR_DONTRECORD) && strlen(g_sConfigFile) > 0)
+ {
+ // Reset the results
+ g_iLastFindResult = -1;
+ g_iLastAppendResult = -1;
+
+
+ // Add it if not found
+ char buffer[64];
+
+ g_iLastFindResult = AutoExecConfig_FindValue(name, buffer, sizeof(buffer), true);
+
+ // We only add this convar if it doesn't exist, or the file doesn't exist and it should be auto-generated
+ if (g_iLastFindResult == AUTOEXEC_FIND_NOT_FOUND || (g_iLastFindResult == AUTOEXEC_FIND_FILE_NOT_FOUND && g_bCreateFile))
+ {
+ g_iLastAppendResult = AutoExecConfig_AppendValue(name, defaultValue, description, flags, hasMin, min, hasMax, max);
+ }
+ }
+
+
+ // Create the convar
+ return CreateConVar(name, defaultValue, description, flags, hasMin, min, hasMax, max);
+}
+
+
+
+
+/**
+ * Executes the autoconfigfile and adds it to the OnConfigsExecuted forward.
+ * If we didn't create it ourselves we let SourceMod create it.
+ *
+ * @noreturn
+*/
+stock void AutoExecConfig_ExecuteFile()
+{
+ // Only let sourcemod create the file, if we didn't do that already.
+ AutoExecConfig(!g_bCreateFile, g_sRawFileName, g_sFolderPath);
+}
+
+
+
+
+
+/**
+ * Formats a autoconfigfile, prefixes path and adds .cfg extension if missing.
+ *
+ * @param buffer String to format.
+ * @param size Maximum size of buffer.
+ * @return Returns one of the AUTOEXEC_FORMAT values..
+*/
+stock static int AutoExecConfig_FormatFileName(char[] buffer, int size, char[] folder="sourcemod")
+{
+ // No config set
+ if (strlen(g_sConfigFile) < 1)
+ {
+ return AUTOEXEC_NO_CONFIG;
+ }
+
+
+ // Can't be an cfgfile
+ if (StrContains(g_sConfigFile, ".cfg") != -1 && strlen(g_sConfigFile) < 4)
+ {
+ return AUTOEXEC_FORMAT_BAD_FILENAME;
+ }
+
+
+ // Pathprefix
+ char pathprefixbuffer[PLATFORM_MAX_PATH];
+ if (strlen(folder) > 0)
+ {
+ Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/%s/", folder);
+
+ if (g_bCreateDirectory && !DirExists(pathprefixbuffer))
+ {
+ CreateDirectory(pathprefixbuffer, g_bCreateDirectoryMode);
+ }
+ }
+ else
+ {
+ Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/");
+ }
+
+
+ char filebuffer[PLATFORM_MAX_PATH];
+ filebuffer[0] = '\0';
+
+ // Add path if file doesn't begin with it
+ if (StrContains(buffer, pathprefixbuffer) != 0)
+ {
+ StrCat(filebuffer, sizeof(filebuffer), pathprefixbuffer);
+ }
+
+ StrCat(filebuffer, sizeof(filebuffer), g_sConfigFile);
+
+
+ // Add .cfg extension if file doesn't end with it
+ if (StrContains(filebuffer[strlen(filebuffer) - 4], ".cfg") != 0)
+ {
+ StrCat(filebuffer, sizeof(filebuffer), ".cfg");
+ }
+
+ strcopy(buffer, size, filebuffer);
+
+ return AUTOEXEC_FORMAT_SUCCESS;
+}
+
+
+
+
+
+
+/**
+ * Appends a convar to the global autoconfigfile
+ *
+ * @param name Name of new convar.
+ * @param defaultValue String containing the default value of new convar.
+ * @param description Optional description of the convar.
+ * @param flags Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details.
+ * @param hasMin Optional boolean that determines if the convar has a minimum value.
+ * @param min Minimum floating point value that the convar can have if hasMin is true.
+ * @param hasMax Optional boolean that determines if the convar has a maximum value.
+ * @param max Maximum floating point value that the convar can have if hasMax is true.
+ * @return Returns one of the AUTOEXEC_APPEND values
+*/
+stock int AutoExecConfig_AppendValue(const char[] name, const char[] defaultValue, const char[] description, int flags, bool hasMin, float min, bool hasMax, float max)
+{
+ // No config set
+ if (strlen(g_sConfigFile) < 1)
+ {
+ return AUTOEXEC_NO_CONFIG;
+ }
+
+
+ char filebuffer[PLATFORM_MAX_PATH];
+ strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile);
+
+
+ //PrintToServer("pathbuffer: %s", filebuffer);
+
+ bool bFileExists = FileExists(filebuffer);
+
+ if (g_bCreateFile || bFileExists)
+ {
+ // If the file already exists we open it in append mode, otherwise we use a write mode which creates the file
+ File fFile = OpenFile(filebuffer, (bFileExists ? "a" : "w"));
+ char writebuffer[2048];
+
+
+ if (fFile == null)
+ {
+ return AUTOEXEC_APPEND_BAD_HANDLE;
+ }
+
+ // We just created the file, so add some header about version and stuff
+ if (g_bCreateFile && !bFileExists)
+ {
+ fFile.WriteLine( "// This file was auto-generated by AutoExecConfig v%s (%s)", AUTOEXECCONFIG_VERSION, AUTOEXECCONFIG_URL);
+
+ GetPluginFilename(g_hPluginHandle, writebuffer, sizeof(writebuffer));
+ Format(writebuffer, sizeof(writebuffer), "// ConVars for plugin \"%s\"", writebuffer);
+ fFile.WriteLine(writebuffer);
+ }
+
+ // Spacer
+ fFile.WriteLine("\n");
+
+
+ // This is used for multiline comments
+ int newlines = GetCharCountInStr('\n', description);
+ if (newlines == 0)
+ {
+ // We have no newlines, we can write the description to the file as is
+ Format(writebuffer, sizeof(writebuffer), "// %s", description);
+ fFile.WriteLine(writebuffer);
+ }
+ else
+ {
+ char[][] newlineBuf = new char[newlines +1][2048];
+ ExplodeString(description, "\n", newlineBuf, newlines +1, 2048, false);
+
+ // Each newline gets a commented newline
+ for (int i; i <= newlines; i++)
+ {
+ if (strlen(newlineBuf[i]) > 0)
+ {
+ fFile.WriteLine("// %s", newlineBuf[i]);
+ }
+ }
+ }
+
+
+ // Descspacer
+ fFile.WriteLine("// -");
+
+
+ // Default
+ Format(writebuffer, sizeof(writebuffer), "// Default: \"%s\"", defaultValue);
+ fFile.WriteLine(writebuffer);
+
+
+ // Minimum
+ if (hasMin)
+ {
+ Format(writebuffer, sizeof(writebuffer), "// Minimum: \"%f\"", min);
+ fFile.WriteLine(writebuffer);
+ }
+
+
+ // Maximum
+ if (hasMax)
+ {
+ Format(writebuffer, sizeof(writebuffer), "// Maximum: \"%f\"", max);
+ fFile.WriteLine(writebuffer);
+ }
+
+
+ // Write end and defaultvalue
+ Format(writebuffer, sizeof(writebuffer), "%s \"%s\"", name, defaultValue);
+ fFile.WriteLine(writebuffer);
+
+
+ fFile.Close();
+
+ return AUTOEXEC_APPEND_SUCCESS;
+ }
+
+ return AUTOEXEC_APPEND_FILE_NOT_FOUND;
+}
+
+
+
+
+
+
+/**
+ * Returns a convar's value from the global autoconfigfile
+ *
+ * @param cvar Cvar to search for.
+ * @param value Buffer to store result into.
+ * @param size Maximum size of buffer.
+ * @param caseSensitive Whether or not the search should be case sensitive.
+ * @return Returns one of the AUTOEXEC_FIND values
+*/
+stock int AutoExecConfig_FindValue(const char[] cvar, char[] value, int size, bool caseSensitive=false)
+{
+ // Security for decl users
+ value[0] = '\0';
+
+
+ // No config set
+ if (strlen(g_sConfigFile) < 1)
+ {
+ return AUTOEXEC_NO_CONFIG;
+ }
+
+
+ char filebuffer[PLATFORM_MAX_PATH];
+ strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile);
+
+
+
+ //PrintToServer("pathbuffer: %s", filebuffer);
+
+ bool bFileExists = FileExists(filebuffer);
+
+ // We want to create the config file and it doesn't exist yet.
+ if (g_bCreateFile && !bFileExists)
+ {
+ return AUTOEXEC_FIND_FILE_NOT_FOUND;
+ }
+
+
+ if (bFileExists)
+ {
+ File fFile = OpenFile(filebuffer, "r");
+ int valuestart;
+ int valueend;
+ int cvarend;
+
+ // Just an reminder to self, leave the values that high
+ char sConvar[64];
+ char sValue[64];
+ char readbuffer[2048];
+ char copybuffer[2048];
+
+ if (fFile == null)
+ {
+ return AUTOEXEC_FIND_BAD_HANDLE;
+ }
+
+
+ while (!fFile.EndOfFile() && fFile.ReadLine(readbuffer, sizeof(readbuffer)))
+ {
+ // Is a comment or not valid
+ if (IsCharSpace(readbuffer[0]) || readbuffer[0] == '/' || (!IsCharNumeric(readbuffer[0]) && !IsCharAlpha(readbuffer[0])) )
+ {
+ continue;
+ }
+
+
+ // Has not enough spaces, must have at least 1
+ if (GetCharCountInStr(' ', readbuffer) < 1)
+ {
+ continue;
+ }
+
+
+ // Ignore cvars which aren't quoted
+ if (GetCharCountInStr('"', readbuffer) != 2)
+ {
+ continue;
+ }
+
+
+
+ // Get the start of the value
+ if ( (valuestart = StrContains(readbuffer, "\"")) == -1 )
+ {
+ continue;
+ }
+
+
+ // Get the end of the value
+ if ( (valueend = StrContains(readbuffer[valuestart+1], "\"")) == -1 )
+ {
+ continue;
+ }
+
+
+ // Get the start of the cvar,
+ if ( (cvarend = StrContains(readbuffer, " ")) == -1 || cvarend >= valuestart)
+ {
+ continue;
+ }
+
+
+ // Skip if cvarendindex is before valuestartindex
+ if (cvarend >= valuestart)
+ {
+ continue;
+ }
+
+
+ // Convar
+ // Tempcopy for security
+ strcopy(copybuffer, sizeof(copybuffer), readbuffer);
+ copybuffer[cvarend] = '\0';
+
+ strcopy(sConvar, sizeof(sConvar), copybuffer);
+
+
+ // Value
+ // Tempcopy for security
+ strcopy(copybuffer, sizeof(copybuffer), readbuffer[valuestart+1]);
+ copybuffer[valueend] = '\0';
+
+ strcopy(sValue, sizeof(sValue), copybuffer);
+
+
+ //PrintToServer("Cvar %s has a value of %s", sConvar, sValue);
+
+ if (StrEqual(sConvar, cvar, caseSensitive))
+ {
+ Format(value, size, "%s", sConvar);
+
+ fFile.Close();
+ return AUTOEXEC_FIND_SUCCESS;
+ }
+ }
+
+ fFile.Close();
+ return AUTOEXEC_FIND_NOT_FOUND;
+ }
+
+
+ return AUTOEXEC_FIND_FILE_NOT_FOUND;
+}
+
+
+
+
+
+
+/**
+ * Cleans the global autoconfigfile from too much spaces
+ *
+ * @return One of the AUTOEXEC_CLEAN values.
+*/
+stock int AutoExecConfig_CleanFile()
+{
+ // No config set
+ if (strlen(g_sConfigFile) < 1)
+ {
+ return AUTOEXEC_NO_CONFIG;
+ }
+
+
+ char sfile[PLATFORM_MAX_PATH];
+ strcopy(sfile, sizeof(sfile), g_sConfigFile);
+
+
+ // Security
+ if (!FileExists(sfile))
+ {
+ return AUTOEXEC_CLEAN_FILE_NOT_FOUND;
+ }
+
+
+
+ char sfile2[PLATFORM_MAX_PATH];
+ Format(sfile2, sizeof(sfile2), "%s_tempcopy", sfile);
+
+
+ char readbuffer[2048];
+ int count;
+ bool firstreached;
+
+
+ // Open files
+ File fFile1 = OpenFile(sfile, "r");
+ File fFile2 = OpenFile(sfile2, "w");
+
+
+
+ // Check filehandles
+ if (fFile1 == null || fFile2 == null)
+ {
+ if (fFile1 != null)
+ {
+ //PrintToServer("Handle1 invalid");
+ fFile1.Close();
+ }
+
+ if (fFile2 != null)
+ {
+ //PrintToServer("Handle2 invalid");
+ fFile2.Close();
+ }
+
+ return AUTOEXEC_CLEAN_BAD_HANDLE;
+ }
+
+
+
+ while (!fFile1.EndOfFile() && fFile1.ReadLine(readbuffer, sizeof(readbuffer)))
+ {
+ // Is space
+ if (IsCharSpace(readbuffer[0]))
+ {
+ count++;
+ }
+ // No space, count from start
+ else
+ {
+ count = 0;
+ }
+
+
+ // Don't write more than 1 space if seperation after informations have been reached
+ if (count < 2 || !firstreached)
+ {
+ ReplaceString(readbuffer, sizeof(readbuffer), "\n", "");
+ fFile2.WriteLine(readbuffer);
+ }
+
+
+ // First bigger seperation after informations has been reached
+ if (count == 2)
+ {
+ firstreached = true;
+ }
+ }
+
+
+ fFile1.Close();
+ fFile2.Close();
+
+
+ // This might be a risk, for now it works
+ DeleteFile(sfile);
+ RenameFile(sfile, sfile2);
+
+ return AUTOEXEC_CLEAN_SUCCESS;
+}
+
+
+
+
+
+
+/**
+ * Returns how many times the given char occures in the given string.
+ *
+ * @param str String to search for in.
+ * @return Occurences of the given char found in string.
+*/
+stock static int GetCharCountInStr(int character, const char[] str)
+{
+ int len = strlen(str);
+ int count;
+
+ for (int i; i < len; i++)
+ {
+ if (str[i] == character)
+ {
+ count++;
+ }
+ }
+
+ return count;
+}
+
+
+
+
+
+
+#pragma deprecated
+stock bool AutoExecConfig_CacheConvars()
+{
+ return false;
+}
diff --git a/sourcemod/scripting/include/colors.inc b/sourcemod/scripting/include/colors.inc
new file mode 100644
index 0000000..3ce8b6a
--- /dev/null
+++ b/sourcemod/scripting/include/colors.inc
@@ -0,0 +1,945 @@
+/**************************************************************************
+ * *
+ * Colored Chat Functions *
+ * Author: exvel, Editor: Popoklopsi, Powerlord, Bara *
+ * Version: 1.2.3 *
+ * by modified by 1NutWunDeR *
+ **************************************************************************/
+
+/*Info: purple works only with CPrintToChat and CPrintToChatAll and without {blue} in the same string
+ (volvo gave them the same color code and saytext2 overrides purple with your current teamcolor.)*/
+
+#if defined _colors_included
+ #endinput
+#endif
+#define _colors_included
+
+#define MAX_MESSAGE_LENGTH 320
+#define MAX_COLORS 17
+
+#define SERVER_INDEX 0
+#define NO_INDEX -1
+#define NO_PLAYER -2
+
+enum Colors
+{
+ Color_Default = 0,
+ Color_Darkred,
+ Color_Green,
+ Color_Lightgreen,
+ Color_Red,
+ Color_Blue,
+ Color_Olive,
+ Color_Lime,
+ Color_Orange,
+ Color_Purple,
+ Color_Grey,
+ Color_Yellow,
+ Color_Lightblue,
+ Color_Steelblue,
+ Color_Darkblue,
+ Color_Pink,
+ Color_Lightred,
+}
+
+/* Colors' properties */
+// {"{default}", "{darkred}", "{green}", "{lightgreen}", "{orange}", "{blue}", "{olive}", "{lime}", "{red}", "{purple}", "{grey}", "{yellow}", "{lightblue}", "{steelblue}", "{darkblue}", "{pink}", "{lightred}"};
+new String:CTag[][] = {"{d}", "{dr}", "{gr}", "{lg}", "{o}", "{b}", "{ol}", "{l}", "{r}", "{p}", "{g}", "{y}", "{lb}", "{sb}", "{db}", "{pi}", "{lr}"};
+new String:CTagCode[][] = {"\x01", "\x02", "\x04", "\x03", "\x03", "\x03", "\x05", "\x06", "\x07", "\x03", "\x08", "\x09","\x0A","\x0B","\x0C","\x0E","\x0F"};
+new bool:CTagReqSayText2[] = {false, false, false, true, true, true, false, false, false, false, false, false, false, false, false, false, false};
+new bool:CEventIsHooked = false;
+new bool:CSkipList[MAXPLAYERS+1] = {false,...};
+
+/* Game default profile */
+new bool:CProfile_Colors[] = {true, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false};
+new CProfile_TeamIndex[] = {NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX, NO_INDEX};
+new bool:CProfile_SayText2 = false;
+
+
+static Handle:sm_show_activity = INVALID_HANDLE;
+
+/**
+ * Prints a message to a specific client in the chat area.
+ * Supports color tags.
+ *
+ * @param client Client index.
+ * @param szMessage Message (formatting rules).
+ * @return No return
+ *
+ * On error/Errors: If the client is not connected an error will be thrown.
+ */
+stock CPrintToChat(client, const String:szMessage[], any:...)
+{
+ if (client <= 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ if (!IsClientInGame(client))
+ ThrowError("Client %d is not in game", client);
+
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+ decl String:szCMessage[MAX_MESSAGE_LENGTH];
+
+ SetGlobalTransTarget(client);
+
+ Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage);
+ VFormat(szCMessage, sizeof(szCMessage), szBuffer, 3);
+
+ new index = CFormat(szCMessage, sizeof(szCMessage));
+
+ if (index == NO_INDEX)
+ PrintToChat(client, "%s", szCMessage);
+ else
+ CSayText2(client, index, szCMessage);
+}
+
+/**
+ * Reples to a message in a command. A client index of 0 will use PrintToServer().
+ * If the command was from the console, PrintToConsole() is used. If the command was from chat, CPrintToChat() is used.
+ * Supports color tags.
+ *
+ * @param client Client index, or 0 for server.
+ * @param szMessage Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @return No return
+ *
+ * On error/Errors: If the client is not connected or invalid.
+ */
+stock CReplyToCommand(client, const String:szMessage[], any:...)
+{
+ decl String:szCMessage[MAX_MESSAGE_LENGTH];
+ SetGlobalTransTarget(client);
+ VFormat(szCMessage, sizeof(szCMessage), szMessage, 3);
+
+ if (client == 0)
+ {
+ CRemoveTags(szCMessage, sizeof(szCMessage));
+ PrintToServer("%s", szCMessage);
+ }
+ else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE)
+ {
+ CRemoveTags(szCMessage, sizeof(szCMessage));
+ PrintToConsole(client, "%s", szCMessage);
+ }
+ else
+ {
+ CPrintToChat(client, "%s", szCMessage);
+ }
+}
+
+/**
+ * Reples to a message in a command. A client index of 0 will use PrintToServer().
+ * If the command was from the console, PrintToConsole() is used. If the command was from chat, CPrintToChat() is used.
+ * Supports color tags.
+ *
+ * @param client Client index, or 0 for server.
+ * @param author Author index whose color will be used for teamcolor tag.
+ * @param szMessage Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @return No return
+ *
+ * On error/Errors: If the client is not connected or invalid.
+ */
+stock CReplyToCommandEx(client, author, const String:szMessage[], any:...)
+{
+ decl String:szCMessage[MAX_MESSAGE_LENGTH];
+ SetGlobalTransTarget(client);
+ VFormat(szCMessage, sizeof(szCMessage), szMessage, 4);
+
+ if (client == 0)
+ {
+ CRemoveTags(szCMessage, sizeof(szCMessage));
+ PrintToServer("%s", szCMessage);
+ }
+ else if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE)
+ {
+ CRemoveTags(szCMessage, sizeof(szCMessage));
+ PrintToConsole(client, "%s", szCMessage);
+ }
+ else
+ {
+ CPrintToChatEx(client, author, "%s", szCMessage);
+ }
+}
+
+/**
+ * Prints a message to all clients in the chat area.
+ * Supports color tags.
+ *
+ * @param client Client index.
+ * @param szMessage Message (formatting rules)
+ * @return No return
+ */
+stock CPrintToChatAll(const String:szMessage[], any:...)
+{
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+
+ for (new i = 1; i <= MaxClients; i++)
+ {
+ if (IsClientInGame(i) && !IsFakeClient(i) && !CSkipList[i])
+ {
+ SetGlobalTransTarget(i);
+ VFormat(szBuffer, sizeof(szBuffer), szMessage, 2);
+
+ CPrintToChat(i, "%s", szBuffer);
+ }
+
+ CSkipList[i] = false;
+ }
+}
+
+/**
+ * Prints a message to a specific client in the chat area.
+ * Supports color tags and teamcolor tag.
+ *
+ * @param client Client index.
+ * @param author Author index whose color will be used for teamcolor tag.
+ * @param szMessage Message (formatting rules).
+ * @return No return
+ *
+ * On error/Errors: If the client or author are not connected an error will be thrown.
+ */
+stock CPrintToChatEx(client, author, const String:szMessage[], any:...)
+{
+ if (client <= 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ if (!IsClientInGame(client))
+ ThrowError("Client %d is not in game", client);
+
+ if (author < 0 || author > MaxClients)
+ ThrowError("Invalid client index %d", author);
+
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+ decl String:szCMessage[MAX_MESSAGE_LENGTH];
+
+ SetGlobalTransTarget(client);
+
+ Format(szBuffer, sizeof(szBuffer), "\x01%s", szMessage);
+ VFormat(szCMessage, sizeof(szCMessage), szBuffer, 4);
+
+ new index = CFormat(szCMessage, sizeof(szCMessage), author);
+
+ if (index == NO_INDEX)
+ PrintToChat(client, "%s", szCMessage);
+ else
+ CSayText2(client, author, szCMessage);
+}
+
+/**
+ * Prints a message to all clients in the chat area.
+ * Supports color tags and teamcolor tag.
+ *
+ * @param author Author index whos color will be used for teamcolor tag.
+ * @param szMessage Message (formatting rules).
+ * @return No return
+ *
+ * On error/Errors: If the author is not connected an error will be thrown.
+ */
+stock CPrintToChatAllEx(author, const String:szMessage[], any:...)
+{
+ if (author < 0 || author > MaxClients)
+ ThrowError("Invalid client index %d", author);
+
+ if (!IsClientInGame(author))
+ ThrowError("Client %d is not in game", author);
+
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+
+ for (new i = 1; i <= MaxClients; i++)
+ {
+ if (IsClientInGame(i) && !IsFakeClient(i) && !CSkipList[i])
+ {
+ SetGlobalTransTarget(i);
+ VFormat(szBuffer, sizeof(szBuffer), szMessage, 3);
+
+ CPrintToChatEx(i, author, "%s", szBuffer);
+ }
+
+ CSkipList[i] = false;
+ }
+}
+
+/**
+ * Removes color tags from the string.
+ *
+ * @param szMessage String.
+ * @return No return
+ */
+stock CRemoveTags(String:szMessage[], maxlength)
+{
+ for (new i = 0; i < MAX_COLORS; i++)
+ ReplaceString(szMessage, maxlength, CTag[i], "", false);
+
+ ReplaceString(szMessage, maxlength, "{teamcolor}", "", false);
+}
+
+/**
+ * Checks whether a color is allowed or not
+ *
+ * @param tag Color Tag.
+ * @return True when color is supported, otherwise false
+ */
+stock CColorAllowed(Colors:color)
+{
+ if (!CEventIsHooked)
+ {
+ CSetupProfile();
+
+ CEventIsHooked = true;
+ }
+
+ return CProfile_Colors[color];
+}
+
+/**
+ * Replace the color with another color
+ * Handle with care!
+ *
+ * @param color color to replace.
+ * @param newColor color to replace with.
+ * @noreturn
+ */
+stock CReplaceColor(Colors:color, Colors:newColor)
+{
+ if (!CEventIsHooked)
+ {
+ CSetupProfile();
+
+ CEventIsHooked = true;
+ }
+
+ CProfile_Colors[color] = CProfile_Colors[newColor];
+ CProfile_TeamIndex[color] = CProfile_TeamIndex[newColor];
+
+ CTagReqSayText2[color] = CTagReqSayText2[newColor];
+ Format(CTagCode[color], sizeof(CTagCode[]), CTagCode[newColor])
+}
+
+/**
+ * This function should only be used right in front of
+ * CPrintToChatAll or CPrintToChatAllEx and it tells
+ * to those funcions to skip specified client when printing
+ * message to all clients. After message is printed client will
+ * no more be skipped.
+ *
+ * @param client Client index
+ * @return No return
+ */
+stock CSkipNextClient(client)
+{
+ if (client <= 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ CSkipList[client] = true;
+}
+
+/**
+ * Replaces color tags in a string with color codes
+ *
+ * @param szMessage String.
+ * @param maxlength Maximum length of the string buffer.
+ * @return Client index that can be used for SayText2 author index
+ *
+ * On error/Errors: If there is more then one team color is used an error will be thrown.
+ */
+stock CFormat(String:szMessage[], maxlength, author=NO_INDEX)
+{
+ decl String:szGameName[30];
+
+ GetGameFolderName(szGameName, sizeof(szGameName));
+
+ /* Hook event for auto profile setup on map start */
+ if (!CEventIsHooked)
+ {
+ CSetupProfile();
+ HookEvent("server_spawn", CEvent_MapStart, EventHookMode_PostNoCopy);
+
+ CEventIsHooked = true;
+ }
+
+ new iRandomPlayer = NO_INDEX;
+
+ // On CS:GO set invisible precolor
+ if (StrEqual(szGameName, "csgo", false))
+ Format(szMessage, maxlength, " \x01\x0B\x01%s", szMessage);
+
+ /* If author was specified replace {teamcolor} tag */
+ if (author != NO_INDEX)
+ {
+ if (CProfile_SayText2)
+ {
+ ReplaceString(szMessage, maxlength, "{teamcolor}", "\x03", false);
+
+ iRandomPlayer = author;
+ }
+ /* If saytext2 is not supported by game replace {teamcolor} with green tag */
+ else
+ ReplaceString(szMessage, maxlength, "{teamcolor}", CTagCode[Color_Green], false);
+ }
+ else
+ ReplaceString(szMessage, maxlength, "{teamcolor}", "", false);
+
+ /* For other color tags we need a loop */
+ for (new i = 0; i < MAX_COLORS; i++)
+ {
+ /* If tag not found - skip */
+ if (StrContains(szMessage, CTag[i], false) == -1)
+ continue;
+
+ /* If tag is not supported by game replace it with green tag */
+ else if (!CProfile_Colors[i])
+ ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green], false);
+
+ /* If tag doesn't need saytext2 simply replace */
+ else if (!CTagReqSayText2[i])
+ ReplaceString(szMessage, maxlength, CTag[i], CTagCode[i], false);
+
+ /* Tag needs saytext2 */
+ else
+ {
+ /* If saytext2 is not supported by game replace tag with green tag */
+ if (!CProfile_SayText2)
+ ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green], false);
+
+ /* Game supports saytext2 */
+ else
+ {
+ /* If random player for tag wasn't specified replace tag and find player */
+ if (iRandomPlayer == NO_INDEX)
+ {
+ /* Searching for valid client for tag */
+ iRandomPlayer = CFindRandomPlayerByTeam(CProfile_TeamIndex[i]);
+
+ /* If player not found replace tag with green color tag */
+ if (iRandomPlayer == NO_PLAYER)
+ ReplaceString(szMessage, maxlength, CTag[i], CTagCode[Color_Green], false);
+
+ /* If player was found simply replace */
+ else
+ ReplaceString(szMessage, maxlength, CTag[i], CTagCode[i], false);
+
+ }
+ /* If found another team color tag throw error */
+ else
+ {
+ //ReplaceString(szMessage, maxlength, CTag[i], "");
+ ThrowError("Using two team colors in one message is not allowed");
+ }
+ }
+
+ }
+ }
+
+ return iRandomPlayer;
+}
+
+/**
+ * Founds a random player with specified team
+ *
+ * @param color_team Client team.
+ * @return Client index or NO_PLAYER if no player found
+ */
+stock CFindRandomPlayerByTeam(color_team)
+{
+ if (color_team == SERVER_INDEX)
+ return 0;
+ else
+ {
+ for (new i = 1; i <= MaxClients; i++)
+ {
+ if (IsClientInGame(i) && GetClientTeam(i) == color_team)
+ return i;
+ }
+ }
+
+ return NO_PLAYER;
+}
+
+/**
+ * Sends a SayText2 usermessage to a client
+ *
+ * @param szMessage Client index
+ * @param maxlength Author index
+ * @param szMessage Message
+ * @return No return.
+ */
+stock CSayText2(client, author, const String:szMessage[])
+{
+ new Handle:hBuffer = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS);
+
+ if(GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf)
+ {
+ PbSetInt(hBuffer, "ent_idx", author);
+ PbSetBool(hBuffer, "chat", true);
+ PbSetString(hBuffer, "msg_name", szMessage);
+ PbAddString(hBuffer, "params", "");
+ PbAddString(hBuffer, "params", "");
+ PbAddString(hBuffer, "params", "");
+ PbAddString(hBuffer, "params", "");
+ }
+ else
+ {
+ BfWriteByte(hBuffer, author);
+ BfWriteByte(hBuffer, true);
+ BfWriteString(hBuffer, szMessage);
+ }
+
+ EndMessage();
+}
+
+/**
+ * Creates game color profile
+ * This function must be edited if you want to add more games support
+ *
+ * @return No return.
+ */
+stock CSetupProfile()
+{
+ decl String:szGameName[30];
+ GetGameFolderName(szGameName, sizeof(szGameName));
+
+ if (StrEqual(szGameName, "cstrike", false))
+ {
+ CProfile_Colors[Color_Lightgreen] = true;
+ CProfile_Colors[Color_Orange] = true;
+ CProfile_Colors[Color_Blue] = true;
+ CProfile_Colors[Color_Olive] = true;
+ CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX;
+ CProfile_TeamIndex[Color_Orange] = 2;
+ CProfile_TeamIndex[Color_Blue] = 3;
+ CProfile_SayText2 = true;
+ }
+ else if (StrEqual(szGameName, "csgo", false))
+ {
+ CProfile_Colors[Color_Red] = true;
+ CProfile_Colors[Color_Blue] = true;
+ CProfile_Colors[Color_Olive] = true;
+ CProfile_Colors[Color_Darkred] = true;
+ CProfile_Colors[Color_Lime] = true;
+ CProfile_Colors[Color_Purple] = true;
+ CProfile_Colors[Color_Grey] = true;
+ CProfile_Colors[Color_Yellow] = true;
+ CProfile_Colors[Color_Lightblue] = true;
+ CProfile_Colors[Color_Steelblue] = true;
+ CProfile_Colors[Color_Darkblue] = true;
+ CProfile_Colors[Color_Pink] = true;
+ CProfile_Colors[Color_Lightred] = true;
+ CProfile_TeamIndex[Color_Orange] = 2;
+ CProfile_TeamIndex[Color_Blue] = 3;
+ CProfile_SayText2 = true;
+ }
+ else if (StrEqual(szGameName, "tf", false))
+ {
+ CProfile_Colors[Color_Lightgreen] = true;
+ CProfile_Colors[Color_Orange] = true;
+ CProfile_Colors[Color_Blue] = true;
+ CProfile_Colors[Color_Olive] = true;
+ CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX;
+ CProfile_TeamIndex[Color_Orange] = 2;
+ CProfile_TeamIndex[Color_Blue] = 3;
+ CProfile_SayText2 = true;
+ }
+ else if (StrEqual(szGameName, "left4dead", false) || StrEqual(szGameName, "left4dead2", false))
+ {
+ CProfile_Colors[Color_Lightgreen] = true;
+ CProfile_Colors[Color_Orange] = true;
+ CProfile_Colors[Color_Blue] = true;
+ CProfile_Colors[Color_Olive] = true;
+ CProfile_TeamIndex[Color_Lightgreen] = SERVER_INDEX;
+ CProfile_TeamIndex[Color_Orange] = 3;
+ CProfile_TeamIndex[Color_Blue] = 2;
+ CProfile_SayText2 = true;
+ }
+ else if (StrEqual(szGameName, "hl2mp", false))
+ {
+ /* hl2mp profile is based on mp_teamplay convar */
+ if (GetConVarBool(FindConVar("mp_teamplay")))
+ {
+ CProfile_Colors[Color_Orange] = true;
+ CProfile_Colors[Color_Blue] = true;
+ CProfile_Colors[Color_Olive] = true;
+ CProfile_TeamIndex[Color_Orange] = 3;
+ CProfile_TeamIndex[Color_Blue] = 2;
+ CProfile_SayText2 = true;
+ }
+ else
+ {
+ CProfile_SayText2 = false;
+ CProfile_Colors[Color_Olive] = true;
+ }
+ }
+ else if (StrEqual(szGameName, "dod", false))
+ {
+ CProfile_Colors[Color_Olive] = true;
+ CProfile_SayText2 = false;
+ }
+ /* Profile for other games */
+ else
+ {
+ if (GetUserMessageId("SayText2") == INVALID_MESSAGE_ID)
+ {
+ CProfile_SayText2 = false;
+ }
+ else
+ {
+ CProfile_Colors[Color_Orange] = true;
+ CProfile_Colors[Color_Blue] = true;
+ CProfile_TeamIndex[Color_Orange] = 2;
+ CProfile_TeamIndex[Color_Blue] = 3;
+ CProfile_SayText2 = true;
+ }
+ }
+}
+
+public Action:CEvent_MapStart(Handle:event, const String:name[], bool:dontBroadcast)
+{
+ CSetupProfile();
+
+ for (new i = 1; i <= MaxClients; i++)
+ CSkipList[i] = false;
+}
+
+/**
+ * Displays usage of an admin command to users depending on the
+ * setting of the sm_show_activity cvar.
+ *
+ * This version does not display a message to the originating client
+ * if used from chat triggers or menus. If manual replies are used
+ * for these cases, then this function will suffice. Otherwise,
+ * CShowActivity2() is slightly more useful.
+ * Supports color tags.
+ *
+ * @param client Client index doing the action, or 0 for server.
+ * @param format Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @noreturn
+ * @error
+ */
+stock CShowActivity(client, const String:format[], any:...)
+{
+ if (sm_show_activity == INVALID_HANDLE)
+ sm_show_activity = FindConVar("sm_show_activity");
+
+ new String:tag[] = "[SM] ";
+
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+ //decl String:szCMessage[MAX_MESSAGE_LENGTH];
+ new value = GetConVarInt(sm_show_activity);
+ new ReplySource:replyto = GetCmdReplySource();
+
+ new String:name[MAX_NAME_LENGTH] = "Console";
+ new String:sign[MAX_NAME_LENGTH] = "ADMIN";
+ new bool:display_in_chat = false;
+ if (client != 0)
+ {
+ if (client < 0 || client > MaxClients || !IsClientConnected(client))
+ ThrowError("Client index %d is invalid", client);
+
+ GetClientName(client, name, sizeof(name));
+ new AdminId:id = GetUserAdmin(client);
+ if (id == INVALID_ADMIN_ID
+ || !GetAdminFlag(id, Admin_Generic, Access_Effective))
+ {
+ sign = "PLAYER";
+ }
+
+ /* Display the message to the client? */
+ if (replyto == SM_REPLY_TO_CONSOLE)
+ {
+ SetGlobalTransTarget(client);
+ VFormat(szBuffer, sizeof(szBuffer), format, 3);
+
+ CRemoveTags(szBuffer, sizeof(szBuffer));
+ PrintToConsole(client, "%s%s\n", tag, szBuffer);
+ display_in_chat = true;
+ }
+ }
+ else
+ {
+ SetGlobalTransTarget(LANG_SERVER);
+ VFormat(szBuffer, sizeof(szBuffer), format, 3);
+
+ CRemoveTags(szBuffer, sizeof(szBuffer));
+ PrintToServer("%s%s\n", tag, szBuffer);
+ }
+
+ if (!value)
+ {
+ return 1;
+ }
+
+ for (new i = 1; i <= MaxClients; i++)
+ {
+ if (!IsClientInGame(i)
+ || IsFakeClient(i)
+ || (display_in_chat && i == client))
+ {
+ continue;
+ }
+ new AdminId:id = GetUserAdmin(i);
+ SetGlobalTransTarget(i);
+ if (id == INVALID_ADMIN_ID
+ || !GetAdminFlag(id, Admin_Generic, Access_Effective))
+ {
+ /* Treat this as a normal user. */
+ if ((value & 1) | (value & 2))
+ {
+ new String:newsign[MAX_NAME_LENGTH];
+ newsign = sign;
+ if ((value & 2) || (i == client))
+ {
+ newsign = name;
+ }
+ VFormat(szBuffer, sizeof(szBuffer), format, 3);
+
+ CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer);
+ }
+ }
+ else
+ {
+ /* Treat this as an admin user */
+ new bool:is_root = GetAdminFlag(id, Admin_Root, Access_Effective);
+ if ((value & 4)
+ || (value & 8)
+ || ((value & 16) && is_root))
+ {
+ new String:newsign[MAX_NAME_LENGTH]
+ newsign = sign;
+ if ((value & 8) || ((value & 16) && is_root) || (i == client))
+ {
+ newsign = name;
+ }
+ VFormat(szBuffer, sizeof(szBuffer), format, 3);
+
+ CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer);
+ }
+ }
+ }
+
+ return 1;
+}
+
+/**
+ * Same as CShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing).
+ * Supports color tags.
+ *
+ * @param client Client index doing the action, or 0 for server.
+ * @param tags Tag to display with.
+ * @param format Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @noreturn
+ * @error
+ */
+stock CShowActivityEx(client, const String:tag[], const String:format[], any:...)
+{
+ if (sm_show_activity == INVALID_HANDLE)
+ sm_show_activity = FindConVar("sm_show_activity");
+
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+ //decl String:szCMessage[MAX_MESSAGE_LENGTH];
+ new value = GetConVarInt(sm_show_activity);
+ new ReplySource:replyto = GetCmdReplySource();
+
+ new String:name[MAX_NAME_LENGTH] = "Console";
+ new String:sign[MAX_NAME_LENGTH] = "ADMIN";
+ new bool:display_in_chat = false;
+ if (client != 0)
+ {
+ if (client < 0 || client > MaxClients || !IsClientConnected(client))
+ ThrowError("Client index %d is invalid", client);
+
+ GetClientName(client, name, sizeof(name));
+ new AdminId:id = GetUserAdmin(client);
+ if (id == INVALID_ADMIN_ID
+ || !GetAdminFlag(id, Admin_Generic, Access_Effective))
+ {
+ sign = "PLAYER";
+ }
+
+ /* Display the message to the client? */
+ if (replyto == SM_REPLY_TO_CONSOLE)
+ {
+ SetGlobalTransTarget(client);
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CRemoveTags(szBuffer, sizeof(szBuffer));
+ PrintToConsole(client, "%s%s\n", tag, szBuffer);
+ display_in_chat = true;
+ }
+ }
+ else
+ {
+ SetGlobalTransTarget(LANG_SERVER);
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CRemoveTags(szBuffer, sizeof(szBuffer));
+ PrintToServer("%s%s\n", tag, szBuffer);
+ }
+
+ if (!value)
+ {
+ return 1;
+ }
+
+ for (new i = 1; i <= MaxClients; i++)
+ {
+ if (!IsClientInGame(i)
+ || IsFakeClient(i)
+ || (display_in_chat && i == client))
+ {
+ continue;
+ }
+ new AdminId:id = GetUserAdmin(i);
+ SetGlobalTransTarget(i);
+ if (id == INVALID_ADMIN_ID
+ || !GetAdminFlag(id, Admin_Generic, Access_Effective))
+ {
+ /* Treat this as a normal user. */
+ if ((value & 1) | (value & 2))
+ {
+ new String:newsign[MAX_NAME_LENGTH];
+ newsign = sign;
+ if ((value & 2) || (i == client))
+ {
+ newsign = name;
+ }
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer);
+ }
+ }
+ else
+ {
+ /* Treat this as an admin user */
+ new bool:is_root = GetAdminFlag(id, Admin_Root, Access_Effective);
+ if ((value & 4)
+ || (value & 8)
+ || ((value & 16) && is_root))
+ {
+ new String:newsign[MAX_NAME_LENGTH];
+ newsign = sign;
+ if ((value & 8) || ((value & 16) && is_root) || (i == client))
+ {
+ newsign = name;
+ }
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer);
+ }
+ }
+ }
+
+ return 1;
+}
+
+/**
+ * Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar.
+ * All users receive a message in their chat text, except for the originating client,
+ * who receives the message based on the current ReplySource.
+ * Supports color tags.
+ *
+ * @param client Client index doing the action, or 0 for server.
+ * @param tags Tag to prepend to the message.
+ * @param format Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @noreturn
+ * @error
+ */
+stock CShowActivity2(client, const String:tag[], const String:format[], any:...)
+{
+ if (sm_show_activity == INVALID_HANDLE)
+ sm_show_activity = FindConVar("sm_show_activity");
+
+ decl String:szBuffer[MAX_MESSAGE_LENGTH];
+ //decl String:szCMessage[MAX_MESSAGE_LENGTH];
+ new value = GetConVarInt(sm_show_activity);
+ GetCmdReplySource();
+
+ new String:name[MAX_NAME_LENGTH] = "Console";
+ new String:sign[MAX_NAME_LENGTH] = "ADMIN";
+ if (client != 0)
+ {
+ if (client < 0 || client > MaxClients || !IsClientConnected(client))
+ ThrowError("Client index %d is invalid", client);
+
+ GetClientName(client, name, sizeof(name));
+ new AdminId:id = GetUserAdmin(client);
+ if (id == INVALID_ADMIN_ID
+ || !GetAdminFlag(id, Admin_Generic, Access_Effective))
+ {
+ sign = "PLAYER";
+ }
+
+ SetGlobalTransTarget(client);
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ /* We don't display directly to the console because the chat text
+ * simply gets added to the console, so we don't want it to print
+ * twice.
+ */
+ CPrintToChatEx(client, client, "%s%s", tag, szBuffer);
+ }
+ else
+ {
+ SetGlobalTransTarget(LANG_SERVER);
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CRemoveTags(szBuffer, sizeof(szBuffer));
+ PrintToServer("%s%s\n", tag, szBuffer);
+ }
+
+ if (!value)
+ {
+ return 1;
+ }
+
+ for (new i = 1; i <= MaxClients; i++)
+ {
+ if (!IsClientInGame(i)
+ || IsFakeClient(i)
+ || i == client)
+ {
+ continue;
+ }
+ new AdminId:id = GetUserAdmin(i);
+ SetGlobalTransTarget(i);
+ if (id == INVALID_ADMIN_ID
+ || !GetAdminFlag(id, Admin_Generic, Access_Effective))
+ {
+ /* Treat this as a normal user. */
+ if ((value & 1) | (value & 2))
+ {
+ new String:newsign[MAX_NAME_LENGTH];
+ newsign = sign;
+ if ((value & 2))
+ {
+ newsign = name;
+ }
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer);
+ }
+ }
+ else
+ {
+ /* Treat this as an admin user */
+ new bool:is_root = GetAdminFlag(id, Admin_Root, Access_Effective);
+ if ((value & 4)
+ || (value & 8)
+ || ((value & 16) && is_root))
+ {
+ new String:newsign[MAX_NAME_LENGTH];
+ newsign = sign;
+ if ((value & 8) || ((value & 16) && is_root))
+ {
+ newsign = name;
+ }
+ VFormat(szBuffer, sizeof(szBuffer), format, 4);
+
+ CPrintToChatEx(i, client, "%s%s: %s", tag, newsign, szBuffer);
+ }
+ }
+ }
+
+ return 1;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/distbugfix.inc b/sourcemod/scripting/include/distbugfix.inc
new file mode 100644
index 0000000..6a7ed18
--- /dev/null
+++ b/sourcemod/scripting/include/distbugfix.inc
@@ -0,0 +1,261 @@
+
+#if defined _distbug_included
+ #endinput
+#endif
+#define _distbug_included
+
+#define DISTBUG_CONFIG_NAME "distbugfix"
+
+#define CHAT_PREFIX "{d}[{l}GC{d}]"
+#define CONSOLE_PREFIX "[GC]"
+#define CHAT_SPACER " {d}|{g} "
+#define DISTBUG_VERSION "2.0.0"
+
+#define MAX_STRAFES 32
+#define MAX_EDGE 32.0
+#define MAX_JUMP_FRAMES 150 // for frame based arrays
+#define MAX_BHOP_FRAMES 8
+
+#define MAX_COOKIE_SIZE 32
+
+enum
+{
+ SETTINGS_DISTBUG_ENABLED = (1 << 0),
+ SETTINGS_SHOW_VEER_BEAM = (1 << 1),
+ SETTINGS_SHOW_JUMP_BEAM = (1 << 2),
+ SETTINGS_SHOW_HUD_GRAPH = (1 << 3),
+ SETTINGS_DISABLE_STRAFE_STATS = (1 << 4),
+ SETTINGS_DISABLE_STRAFE_GRAPH = (1 << 5),
+ SETTINGS_ADV_CHAT_STATS = (1 << 6),
+}
+
+enum JumpType
+{
+ // unprintable jumptypes. only for tracking
+ JUMPTYPE_NONE,
+
+ JUMPTYPE_LJ, // longjump
+ JUMPTYPE_WJ, // weirdjump
+ JUMPTYPE_LAJ, // ladderjump
+ JUMPTYPE_BH, // bunnyhop
+ JUMPTYPE_CBH, // crouched bunnyhop
+};
+
+enum JumpDir
+{
+ JUMPDIR_FORWARDS,
+ JUMPDIR_BACKWARDS,
+ JUMPDIR_LEFT,
+ JUMPDIR_RIGHT,
+};
+
+enum StrafeType
+{
+ STRAFETYPE_OVERLAP, // IN_MOVELEFT and IN_MOVERIGHT are overlapping and sidespeed is 0
+ STRAFETYPE_NONE, // IN_MOVELEFT and IN_MOVERIGHT are both not pressed and sidespeed is 0
+
+ STRAFETYPE_LEFT, // only IN_MOVELEFT is down and sidespeed isn't 0.
+ STRAFETYPE_OVERLAP_LEFT, // IN_MOVELEFT and IN_MOVERIGHT are overlapping, but sidespeed is smaller than 0 (not 0)
+ STRAFETYPE_NONE_LEFT, // IN_MOVELEFT and IN_MOVERIGHT are both not pressed and sidespeed is smaller than 0 (not 0)
+
+ STRAFETYPE_RIGHT, // only IN_MOVERIGHT is down and sidespeed isn't 0.
+ STRAFETYPE_OVERLAP_RIGHT, // IN_MOVELEFT and IN_MOVERIGHT are overlapping, but sidespeed is bigger than 0 (not 0)
+ STRAFETYPE_NONE_RIGHT, // IN_MOVELEFT and IN_MOVERIGHT are both not pressed and sidespeed is bigger than 0 (not 0)
+};
+
+enum JumpBeamColour
+{
+ JUMPBEAM_NEUTRAL, // speed stays the same
+ JUMPBEAM_LOSS, // speed loss
+ JUMPBEAM_GAIN, // speed gain
+ JUMPBEAM_DUCK, // duck key down
+};
+
+enum struct PlayerData
+{
+ int tickCount;
+ int buttons;
+ int lastButtons;
+ int flags;
+ int lastFlags;
+ int framesOnGround;
+ int framesInAir;
+ MoveType movetype;
+ MoveType lastMovetype;
+ float stamina;
+ float lastStamina;
+ float forwardmove;
+ float lastForwardmove;
+ float sidemove;
+ float lastSidemove;
+ float gravity;
+ float angles[3];
+ float lastAngles[3];
+ float position[3];
+ float lastPosition[3];
+ float velocity[3];
+ float lastVelocity[3];
+ float lastGroundPos[3]; // last position where the player left the ground.
+ float ladderNormal[3];
+ bool lastGroundPosWalkedOff;
+ bool landedDucked;
+
+ int prespeedFog;
+ float prespeedStamina;
+
+ float jumpGroundZ;
+ float jumpPos[3];
+ float jumpAngles[3];
+ float landGroundZ;
+ float landPos[3];
+
+ int fwdReleaseFrame;
+ int jumpFrame;
+ bool trackingJump;
+ bool failedJump;
+ bool jumpGotFailstats;
+
+ // jump data
+ JumpType jumpType;
+ JumpType lastJumpType;
+ JumpDir jumpDir;
+ float jumpDistance;
+ float jumpPrespeed;
+ float jumpMaxspeed;
+ float jumpVeer;
+ float jumpAirpath;
+ float jumpSync;
+ float jumpEdge;
+ float jumpLandEdge;
+ float jumpBlockDist;
+ float jumpHeight;
+ float jumpJumpoffAngle;
+ int jumpAirtime;
+ int jumpFwdRelease;
+ int jumpOverlap;
+ int jumpDeadair;
+
+ // strafes!
+ int strafeCount;
+ float strafeSync[MAX_STRAFES];
+ float strafeGain[MAX_STRAFES];
+ float strafeLoss[MAX_STRAFES];
+ float strafeMax[MAX_STRAFES];
+ int strafeAirtime[MAX_STRAFES];
+ int strafeOverlap[MAX_STRAFES];
+ int strafeDeadair[MAX_STRAFES];
+ float strafeAvgGain[MAX_STRAFES];
+
+ float strafeAvgEfficiency[MAX_STRAFES];
+ int strafeAvgEfficiencyCount[MAX_STRAFES]; // how many samples are in strafeAvgEfficiency
+ float strafeMaxEfficiency[MAX_STRAFES];
+
+ StrafeType strafeGraph[MAX_JUMP_FRAMES];
+ float mouseGraph[MAX_JUMP_FRAMES];
+ float jumpBeamX[MAX_JUMP_FRAMES];
+ float jumpBeamY[MAX_JUMP_FRAMES];
+ JumpBeamColour jumpBeamColour[MAX_JUMP_FRAMES];
+}
+
+/**
+ * Check if player is overlapping their MOVERIGHT and MOVELEFT buttons.
+ *
+ * @param x Buttons;
+ * @return True if overlapping, false otherwise.
+ */
+stock bool IsOverlapping(int buttons, JumpDir jumpDir)
+{
+ if (jumpDir == JUMPDIR_FORWARDS || jumpDir == JUMPDIR_BACKWARDS)
+ {
+ return (buttons & IN_MOVERIGHT) && (buttons & IN_MOVELEFT);
+ }
+ // else if (jumpDir == JUMPDIR_LEFT || jumpDir == JUMPDIR_RIGHT)
+ return (buttons & IN_FORWARD) && (buttons & IN_BACK);
+}
+
+/**
+ * Checks if the player is not holding down their MOVERIGHT and MOVELEFT buttons.
+ *
+ * @param x Buttons.
+ * @return True if they're not holding either, false otherwise.
+ */
+stock bool IsDeadAirtime(int buttons, JumpDir jumpDir)
+{
+ if (jumpDir == JUMPDIR_FORWARDS || jumpDir == JUMPDIR_BACKWARDS)
+ {
+ return (!(buttons & IN_MOVERIGHT) && !(buttons & IN_MOVELEFT));
+ }
+ // else if (jumpDir == JUMPDIR_LEFT || jumpDir == JUMPDIR_RIGHT)
+ return (!(buttons & IN_FORWARD) && !(buttons & IN_BACK));
+}
+
+stock void ToggleCVar(ConVar cvar)
+{
+ cvar.BoolValue = !cvar.BoolValue;
+}
+
+stock void GetRealLandingOrigin(float landGroundZ, const float origin[3], const float velocity[3], float result[3])
+{
+ if ((origin[2] - landGroundZ) == 0.0)
+ {
+ result = origin;
+ return;
+ }
+
+ // this is like this because it works
+ float frametime = GetTickInterval();
+ float verticalDistance = origin[2] - (origin[2] + velocity[2] * frametime);
+ float fraction = (origin[2] - landGroundZ) / verticalDistance;
+
+ float addDistance[3];
+ addDistance = velocity;
+ ScaleVector(addDistance, frametime * fraction);
+
+ AddVectors(origin, addDistance, result);
+}
+
+stock int FloatSign(float value)
+{
+ if (value > 0.0)
+ {
+ return 1;
+ }
+ else if (value < 0.0)
+ {
+ return -1;
+ }
+ return 0;
+}
+
+stock int FloatSign2(float value)
+{
+ if (value >= 0.0)
+ {
+ return 1;
+ }
+ // else if (value < 0.0)
+ return -1;
+}
+
+stock void ShowPanel(int client, int duration, const char[] message)
+{
+ Event show_survival_respawn_status = CreateEvent("show_survival_respawn_status");
+ if (show_survival_respawn_status == INVALID_HANDLE)
+ {
+ return;
+ }
+
+ show_survival_respawn_status.SetString("loc_token", message);
+ show_survival_respawn_status.SetInt("duration", duration);
+ show_survival_respawn_status.SetInt("userid", -1);
+
+ if (client == -1)
+ {
+ show_survival_respawn_status.Fire();
+ }
+ else
+ {
+ show_survival_respawn_status.FireToClient(client);
+ show_survival_respawn_status.Cancel();
+ }
+}
diff --git a/sourcemod/scripting/include/gamechaos.inc b/sourcemod/scripting/include/gamechaos.inc
new file mode 100644
index 0000000..eeaf040
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos.inc
@@ -0,0 +1,20 @@
+
+// GameChaos's includes for various specific stuff
+
+#if defined _gamechaos_stocks_included
+ #endinput
+#endif
+#define _gamechaos_stocks_included
+
+#define GC_INCLUDES_VERSION 0x01_00_00
+#define GC_INCLUDES_VERSION_STRING "1.0.0"
+
+#include <gamechaos/arrays>
+#include <gamechaos/client>
+#include <gamechaos/debug>
+#include <gamechaos/maths>
+#include <gamechaos/misc>
+#include <gamechaos/strings>
+#include <gamechaos/tempents>
+#include <gamechaos/tracing>
+#include <gamechaos/vectors> \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/arrays.inc b/sourcemod/scripting/include/gamechaos/arrays.inc
new file mode 100644
index 0000000..eba62bb
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/arrays.inc
@@ -0,0 +1,52 @@
+
+#if defined _gamechaos_stocks_arrays_included
+ #endinput
+#endif
+#define _gamechaos_stocks_arrays_included
+
+#define GC_ARRAYS_VERSION 0x01_00_00
+#define GC_ARRAYS_VERSION_STRING "1.0.0"
+
+/**
+ * Copies an array into an arraylist.
+ *
+ * @param array Arraylist Handle.
+ * @param index Index in the arraylist.
+ * @param values Array to copy.
+ * @param size Size of the array to copy.
+ * @param offset Arraylist offset to set.
+ * @return Number of cells copied.
+ * @error Invalid Handle or invalid index.
+ */
+stock int GCSetArrayArrayIndexOffset(ArrayList array, int index, const any[] values, int size, int offset)
+{
+ int cells;
+ for (int i; i < size; i++)
+ {
+ array.Set(index, values[i], offset + i);
+ cells++;
+ }
+ return cells;
+}
+
+/**
+ * Copies an arraylist's specified cells to an array.
+ *
+ * @param array Arraylist Handle.
+ * @param index Index in the arraylist.
+ * @param result Array to copy to.
+ * @param size Size of the array to copy to.
+ * @param offset Arraylist offset.
+ * @return Number of cells copied.
+ * @error Invalid Handle or invalid index.
+ */
+stock int GCCopyArrayArrayIndex(const ArrayList array, int index, any[] result, int size, int offset)
+{
+ int cells;
+ for (int i = offset; i < (size + offset); i++)
+ {
+ result[i] = array.Get(index, i);
+ cells++;
+ }
+ return cells;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/client.inc b/sourcemod/scripting/include/gamechaos/client.inc
new file mode 100644
index 0000000..cb2114a
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/client.inc
@@ -0,0 +1,300 @@
+
+#if defined _gamechaos_stocks_client_included
+ #endinput
+#endif
+#define _gamechaos_stocks_client_included
+
+#define GC_CLIENT_VERSION 0x01_00_00
+#define GC_CLIENT_VERSION_STRING "1.0.0"
+
+/**
+ * Credit: Don't remember.
+ * Removes a player's weapon from the specified slot.
+ *
+ * @param client Client index.
+ * @param slot Weapon slot.
+ * @return True if removed, false otherwise.
+ */
+stock bool GCRemoveWeaponBySlot(int client, int slot)
+{
+ int entity = GetPlayerWeaponSlot(client, slot);
+ if (IsValidEdict(entity))
+ {
+ RemovePlayerItem(client, entity);
+ AcceptEntityInput(entity, "kill");
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Checks if a client is valid and not the server and optionally, whether he's alive.
+ *
+ * @param client Client index.
+ * @param alive Whether to check alive.
+ * @return True if valid, false otherwise.
+ */
+stock bool GCIsValidClient(int client, bool alive = false)
+{
+ return (client >= 1 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client) && !IsClientSourceTV(client) && (!alive || IsPlayerAlive(client)));
+}
+
+
+
+/**
+ * Gets the value of m_flForwardMove.
+ *
+ * @param client Client index.
+ * @return Value of m_flForwardMove.
+ */
+stock float GCGetClientForwardMove(int client)
+{
+ return GetEntPropFloat(client, Prop_Data, "m_flForwardMove");
+}
+
+/**
+ * Gets the value of m_flSideMove.
+ *
+ * @param client Client index.
+ * @return Value of m_flSideMove.
+ */
+stock float GCGetClientSideMove(int client)
+{
+ return GetEntPropFloat(client, Prop_Data, "m_flSideMove");
+}
+
+/**
+ * Gets the client's abs origin.
+ *
+ * @param client Client index.
+ * @return result Player's origin.
+ */
+stock float[] GCGetClientAbsOriginRet(int client)
+{
+ float result[3]
+ GetClientAbsOrigin(client, result);
+ return result;
+}
+
+/**
+ * Copies the client's velocity to a vector.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void GCGetClientVelocity(int client, float result[3])
+{
+ GetEntPropVector(client, Prop_Data, "m_vecVelocity", result);
+}
+
+/**
+ * Gets the client's velocity (m_vecVelocity).
+ *
+ * @param client Client index.
+ * @return result m_vecVelocity.
+ */
+stock float[] GCGetClientVelocityRet(int client)
+{
+ float result[3]
+ GetEntPropVector(client, Prop_Data, "m_vecVelocity", result);
+ return result
+}
+
+/**
+ * Copies the client's basevelocity to a vector.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void GCGetClientBaseVelocity(int client, float result[3])
+{
+ GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", result);
+}
+
+/**
+ * Gets the client's basevelocity (m_vecBaseVelocity).
+ *
+ * @param client Client index.
+ * @return result m_vecBaseVelocity.
+ */
+stock float[] GCGetClientBaseVelocityRet(int client)
+{
+ float result[3];
+ GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", result);
+ return result;
+}
+
+
+/**
+ * Gets the client's "m_flDuckSpeed" value.
+ *
+ * @param client Client index.
+ * @return "m_flDuckSpeed".
+ */
+stock float GCGetClientDuckSpeed(int client)
+{
+ return GetEntPropFloat(client, Prop_Send, "m_flDuckSpeed");
+}
+
+/**
+ * Gets the client's "m_flDuckAmount" value.
+ *
+ * @param client Client index.
+ * @return "m_flDuckAmount".
+ */
+stock float GCGetClientDuckAmount(int client)
+{
+ return GetEntPropFloat(client, Prop_Send, "m_flDuckAmount");
+}
+
+/**
+ * Gets the client's "m_bDucking" value.
+ *
+ * @param client Client index.
+ * @return "m_bDucking".
+ */
+stock int GCGetClientDucking(int client)
+{
+ return GetEntProp(client, Prop_Data, "m_bDucking");
+}
+
+/**
+ * Gets the client's "m_flMaxspeed" value.
+ *
+ * @param client Client index.
+ * @return "m_flMaxspeed".
+ */
+stock float GCGetClientMaxspeed(int client)
+{
+ return GetEntPropFloat(client, Prop_Send, "m_flMaxspeed");
+}
+
+/**
+ * Gets the client's "m_afButtonPressed" value.
+ *
+ * @param client Client index.
+ * @return "m_afButtonPressed".
+ */
+stock int GCGetClientButtonPressed(int client)
+{
+ return GetEntProp(client, Prop_Data, "m_afButtonPressed");
+}
+
+/**
+ * Gets the client's "m_afButtonReleased" value.
+ *
+ * @param client Client index.
+ * @return "m_afButtonReleased".
+ */
+stock int GCGetClientButtonReleased(int client)
+{
+ return GetEntProp(client, Prop_Data, "m_afButtonReleased");
+}
+
+/**
+ * Gets the client's "m_afButtonLast" value.
+ *
+ * @param client Client index.
+ * @return "m_afButtonLast".
+ */
+stock int GCGetClientButtonLast(int client)
+{
+ return GetEntProp(client, Prop_Data, "m_afButtonLast");
+}
+
+/**
+ * Gets the client's "m_afButtonForced" value.
+ *
+ * @param client Client index.
+ * @return "m_afButtonForced".
+ */
+stock int GCGetClientForcedButtons(int client)
+{
+ return GetEntProp(client, Prop_Data, "m_afButtonForced");
+}
+
+/**
+ * Gets the client's "m_flStamina" value.
+ *
+ * @param client Client index.
+ * @return "m_flStamina".
+ */
+stock float GCGetClientStamina(int client)
+{
+ return GetEntPropFloat(client, Prop_Send, "m_flStamina");
+}
+
+
+
+/**
+ * Sets the client's origin.
+ *
+ * @param client Client index.
+ * @param origin New origin.
+ */
+stock void GCSetClientAbsOrigin(int client, const float origin[3])
+{
+ SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin);
+}
+
+/**
+ * Sets the client's velocity.
+ *
+ * @param client Client index.
+ * @param velocity New velocity.
+ */
+stock void GCSetClientVelocity(int client, const float velocity[3])
+{
+ SetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity);
+}
+
+/**
+ * Sets the client's "m_vecAbsVelocity".
+ *
+ * @param client Client index.
+ * @param velocity New "m_vecAbsVelocity".
+ */
+stock void GCSetClientAbsVelocity(int client, const float velocity[3])
+{
+ SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", velocity);
+}
+
+/**
+ * Sets the client's eye angles.
+ * Ang has to be a 2 member array or more
+ *
+ * @param client Client index.
+ * @param ang New eyeangles.
+ */
+stock void GCSetClientEyeAngles(int client, const float[] ang)
+{
+ SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[0]", ang[0]);
+ SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[1]", ang[1]);
+}
+
+
+/**
+ * Sets the client's "m_flDuckSpeed".
+ *
+ * @param client Client index.
+ * @param value New "m_flDuckSpeed".
+ */
+stock void GCSetClientDuckSpeed(int client, float value)
+{
+ SetEntPropFloat(client, Prop_Send, "m_flDuckSpeed", value);
+}
+
+stock void GCSetClientDuckAmount(int client, float value)
+{
+ SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", value);
+}
+
+stock void GCSetClientForcedButtons(int client, int buttons)
+{
+ SetEntProp(client, Prop_Data, "m_afButtonForced", buttons);
+}
+
+stock void GCSetClientStamina(int client, float stamina)
+{
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", stamina)
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/debug.inc b/sourcemod/scripting/include/gamechaos/debug.inc
new file mode 100644
index 0000000..4e5b7e7
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/debug.inc
@@ -0,0 +1,19 @@
+
+// gamechaos's debug stocks
+// useful stocks for debugging
+
+#if defined _gamechaos_debug_included
+ #endinput
+#endif
+#define _gamechaos_debug_included
+
+#define GC_DEBUG_VERSION 0x1_00_00
+#define GC_DEBUG_VERSION_STRING "1.0.0"
+
+#if defined GC_DEBUG
+ #define GC_ASSERT(%1) if (!(%1))SetFailState("Assertion failed: \""...#%1..."\"")
+ #define GC_DEBUGPRINT(%1) PrintToChatAll(%1)
+#else
+ #define GC_ASSERT(%1)%2;
+ #define GC_DEBUGPRINT(%1)%2;
+#endif
diff --git a/sourcemod/scripting/include/gamechaos/isvalidclient.inc b/sourcemod/scripting/include/gamechaos/isvalidclient.inc
new file mode 100644
index 0000000..bf80246
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/isvalidclient.inc
@@ -0,0 +1,16 @@
+
+#if defined _gamechaos_isvalidclient_client_included
+ #endinput
+#endif
+#define _gamechaos_isvalidclient_client_included
+
+/**
+ * Checks if a client is valid.
+ *
+ * @param client Client index.
+ * @return True if valid, false otherwise.
+ */
+stock bool IsValidClient(int client)
+{
+ return (client >= 0 && client <= MaxClients && IsValidEntity(client) && IsClientConnected(client) && IsClientInGame(client));
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc b/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc
new file mode 100644
index 0000000..0cbb828
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/kreedzclimbing.inc
@@ -0,0 +1,226 @@
+//
+// Useful things for making plugins for Kreedz Climbing
+//
+
+#if defined _gamechaos_kreedzclimbing_included
+ #endinput
+#endif
+#define _gamechaos_kreedzclimbing_included
+
+#define GC_KREEDZCLIMBING_VERSION 0x01_00_00
+#define GC_KREEDZCLIMBING_VERSION_STRING "1.0.0"
+
+
+#define MAX_COURSE_SIZE 128 // Reasonable maximum characters a course name can have
+#define COURSE_CVAR_COUNT 20 // the amount of Course<int> cvars
+
+// Kreedz Climbing Client Commands:
+// These may be executed by a player via the console, with / in chat, or via binds.
+
+// specmode - Cycles spectator mode (F3 by default).
+// kz_pause - Pauses the timer.
+// flare - Fires a flare.
+// gototimer | start - Returns to the last pressed start timer.
+// spectate | spec - Enters spectator mode.
+// forcespectator - Becomes a spectator no matter what (force respawns a dead player as well).
+// stoptimer - Instantly stops the player's timer.
+// climb | ct - Respawns at the map spawnpoint.
+// InvalidateTimer - Invalidates the player's timer. An invalid timer can't earn rewards for completing the course. InvalidateTimer 1 displays the message, without the 1 it does not.
+
+// Kreedz Climbing Constants
+
+// Timer state (player->m_Local->Timer_Active)
+#define TIMER_STATE_INVISIBLE 0
+#define TIMER_STATE_ACTIVE 1
+#define TIMER_STATE_INACTIVE 2
+#define TIMER_STATE_PAUSED 3
+
+// Timer flags (player_manager->m_iTimerFlags[32])
+// These are replicated flags for player's timer (most timer data is local to it's own player).
+// Note that these flags are mirrors of data local to the player - they are set to the player's
+// state every frame and cannot be changed.
+
+#define TIMER_FLAG_INVALID (1 << 0)
+#define TIMER_FLAG_ACTIVE (1 << 1) // We need to broadcast this because Timer_State is local only.
+#define TIMER_FLAG_PAUSED (1 << 2) // A paused timer cannot be active and vice versa.
+
+// Environmental Attributes (player->m_iEnvironmentalAttributes)
+#define PLAYER_ENV_ATTRIBUTES_BHOP (1 << 0)
+#define PLAYER_ENV_ATTRIBUTES_SURF (1 << 1)
+#define PLAYER_ENV_ATTRIBUTES_AUTOBHOP (1 << 2)
+#define PLAYER_ENV_ATTRIBUTES_CSGOMOVEMENT (1 << 3)
+#define PLAYER_ENV_ATTRIBUTES_CSGODUCKHULL (1 << 4)
+
+// Movement restriction flags (player->m_iMovementRestrictions) (new version of Environmental Restrictions below)
+#define PLAYER_MOVEMENT_RESTRICTION_NOJUMP (1 << 0)
+#define PLAYER_MOVEMENT_RESTRICTION_NOBHOP (1 << 1)
+#define PLAYER_MOVEMENT_RESTRICTION_NODOUBLEDUCK (1 << 2)
+
+// OBSOLETE: ONLY IN OLD MAPS: Environmental Restrictions (player->m_iEnvironmentalRestrictions), note not flags, complete integer.
+#define PLAYER_ENV_RESTRICTION_NOJUMP 1
+#define PLAYER_ENV_RESTRICTION_NOBHOP 2
+#define PLAYER_ENV_RESTRICTION_BOTH 3
+
+// Cooperative status (player->m_Local.m_multiplayercoursedata.Player1Status, Player2Status etc)
+#define COOPERATIVE_STATUS_NONE 0
+#define COOPERATIVE_STATUS_WAITING 1
+#define COOPERATIVE_STATUS_READY 2
+#define COOPERATIVE_STATUS_TIMER_ACTIVE 3
+#define COOPERATIVE_STATUS_TIMER_COMPLETE 4
+#define COOPERATIVE_STATUS_PLAYER_DISCONNECTED 5 // Player disconnected from server, waiting for them to reconnect.
+#define COOPERATIVE_STATUS_TIMER_PAUSED 6
+
+// Kreedz Climbing Button Constants
+#define IN_CHECKPOINT (1 << 25)
+#define IN_TELEPORT (1 << 26)
+#define IN_SPECTATE (1 << 27)
+//#define IN_AVAILABLE (1 << 28) // Unused
+#define IN_HOOK (1 << 29)
+
+// converts the course id from the obsolete "player_starttimer" event into the course name
+stock void GCCourseidToString(int courseid, char[] course, int size)
+{
+ char szCourseid[16];
+ if (courseid < 1 || courseid > COURSE_CVAR_COUNT)
+ {
+ return;
+ }
+ FormatEx(szCourseid, sizeof(szCourseid), "Course%i", courseid);
+ FindConVar(szCourseid).GetString(course, size);
+}
+
+stock void GCGetCurrentMapCourses(ArrayList &array)
+{
+ if (array == null)
+ {
+ // 1 for endurance bool
+ array = new ArrayList(ByteCountToCells(MAX_COURSE_SIZE) + 1);
+ }
+ else
+ {
+ array.Clear();
+ }
+
+ char course[MAX_COURSE_SIZE];
+
+ int ent;
+ while((ent = FindEntityByClassname(ent, "func_stoptimer")) != -1)
+ {
+ int courseid = GetEntProp(ent, Prop_Data, "CourseID");
+ GCCourseidToString(courseid, course, sizeof(course));
+ array.PushString(course);
+
+ bool endurance = GCIsCourseEndurance(course, ent);
+ array.Set(array.Length - 1, endurance, ByteCountToCells(MAX_COURSE_SIZE));
+ }
+
+ int courseStringtableCount;
+ int courseNamesIdx = FindStringTable("CourseNames");
+ courseStringtableCount = GetStringTableNumStrings(courseNamesIdx);
+
+ for (int i; i < courseStringtableCount; i++)
+ {
+ ReadStringTable(courseNamesIdx, i, course, sizeof(course));
+ array.PushString(course);
+
+ bool endurance = GCIsCourseEndurance(course, ent);
+ array.Set(array.Length - 1, endurance, ByteCountToCells(MAX_COURSE_SIZE));
+ }
+}
+
+stock int GCGetTimerState(int client)
+{
+ return GetEntProp(client, Prop_Send, "Timer_Active");
+}
+
+stock void GCSetTimerState(int client, int timerstate)
+{
+ SetEntProp(client, Prop_Send, "Timer_Active", timerstate);
+}
+
+stock int GCGetPlayerEnvAttributes(int client)
+{
+ return GetEntProp(client, Prop_Send, "m_iEnvironmentalAttributes");
+}
+
+stock void GCSetPlayerEnvAttributes(int client, int attributes)
+{
+ SetEntProp(client, Prop_Send, "m_iEnvironmentalAttributes", attributes);
+}
+
+stock int GCGetPlayerMovementRestrictions(int client)
+{
+ return GetEntProp(client, Prop_Send, "m_iMovementRestrictions");
+}
+
+stock void GCSetPlayerMovementRestrictions(int client, int restrictions)
+{
+ SetEntProp(client, Prop_Send, "m_iMovementRestrictions", restrictions);
+}
+
+stock void GCSetActiveCourse(int client, int course)
+{
+ int ent = FindEntityByClassname(0, "player_manager");
+ int courseOffset = FindSendPropInfo("CPlayerResource", "m_iActiveCourse");
+ SetEntData(ent, courseOffset + (client * 4), course);
+}
+
+stock int GCGetTimerFlags(int client)
+{
+ int ent = FindEntityByClassname(0, "player_manager");
+ int courseOffset = FindSendPropInfo("CPlayerResource", "m_iTimerFlags");
+ return GetEntData(ent, courseOffset + (client * 4));
+}
+
+stock bool GCInvalidateTimer(int client)
+{
+ if (~GCGetTimerFlags(client) & TIMER_FLAG_INVALID)
+ {
+ FakeClientCommand(client, "InvalidateTimer 1");
+ return true;
+ }
+
+ return false;
+}
+
+stock bool GCIsCourseEndurance(char[] course, int ent = -1)
+{
+ if (ent != -1)
+ {
+ if (IsValidEntity(ent))
+ {
+ return !!(GetEntProp(ent, Prop_Data, "m_bEnduranceCourse"));
+ }
+ }
+
+ while ((ent = FindEntityByClassname(ent, "point_climbtimer")) != -1)
+ {
+ if (IsValidEntity(ent))
+ {
+ char buffer[MAX_COURSE_SIZE];
+ GetEntPropString(ent, Prop_Data, "m_strCourseName", buffer, sizeof(buffer));
+
+ if (StrEqual(buffer, course))
+ {
+ return !!(GetEntProp(ent, Prop_Data, "m_bEnduranceCourse"));
+ }
+ }
+ }
+
+ while ((ent = FindEntityByClassname(ent, "func_stoptimer")) != -1)
+ {
+ if (IsValidEntity(ent))
+ {
+ char buffer[MAX_COURSE_SIZE];
+ int courseid = GetEntProp(ent, Prop_Data, "CourseID");
+ GCCourseidToString(courseid, buffer, sizeof(buffer));
+
+ if (StrEqual(buffer, course))
+ {
+ return !!(GetEntProp(ent, Prop_Data, "m_bEnduranceCourse"));
+ }
+ }
+ }
+
+ return false;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/maths.inc b/sourcemod/scripting/include/gamechaos/maths.inc
new file mode 100644
index 0000000..f3c94af
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/maths.inc
@@ -0,0 +1,362 @@
+
+#if defined _gamechaos_stocks_maths_included
+ #endinput
+#endif
+#define _gamechaos_stocks_maths_included
+
+#define GC_MATHS_VERSION 0x02_00_00
+#define GC_MATHS_VERSION_STRING "2.0.0"
+
+#include <gamechaos/vectors>
+
+#define GC_PI 3.14159265359
+
+#define GC_DEGREES(%1) ((%1) * 180.0 / GC_PI) // convert radians to degrees
+#define GC_RADIANS(%1) ((%1) * GC_PI / 180.0) // convert degrees to radians
+
+#define GC_FLOAT_NAN view_as<float>(0xffffffff)
+#define GC_FLOAT_INFINITY view_as<float>(0x7f800000)
+#define GC_FLOAT_NEGATIVE_INFINITY view_as<float>(0xff800000)
+
+#define GC_FLOAT_LARGEST_POSITIVE view_as<float>(0x7f7fffff)
+#define GC_FLOAT_SMALLEST_NEGATIVE view_as<float>(0xff7fffff)
+
+#define GC_FLOAT_SMALLEST_POSITIVE view_as<float>(0x00000001)
+#define GC_FLOAT_LARGEST_NEGATIVE view_as<float>(0x80000001)
+
+#define GC_INT_MAX 0x7fffffff
+#define GC_INT_MIN 0xffffffff
+
+
+/**
+ * Credit: https://stackoverflow.com/questions/5666222/3d-line-plane-intersection
+ * Determines the point of intersection between a plane defined by a point and a normal vector and a line defined by a point and a direction vector.
+ *
+ * @param planePoint A point on the plane.
+ * @param planeNormal Normal vector of the plane.
+ * @param linePoint A point on the line.
+ * @param lineDirection Direction vector of the line.
+ * @param result Resultant vector.
+ */
+stock void GCLineIntersection(const float planePoint[3], const float planeNormal[3], const float linePoint[3], const float lineDirection[3], float result[3])
+{
+ if (GetVectorDotProduct(planeNormal, lineDirection) == 0)
+ {
+ return;
+ }
+
+ float t = (GetVectorDotProduct(planeNormal, planePoint)
+ - GetVectorDotProduct(planeNormal, linePoint))
+ / GetVectorDotProduct(planeNormal, lineDirection);
+
+ float lineDir[3];
+ lineDir = lineDirection;
+ NormalizeVector(lineDir, lineDir);
+
+ ScaleVector(lineDir, t);
+
+ AddVectors(linePoint, lineDir, result);
+}
+
+/**
+ * Calculates a point according to angles supplied that is a certain distance away.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ * @param distance Maximum distance to trace.
+ * @return True on success, false otherwise.
+ */
+stock void GCCalcPointAngleDistance(const float start[3], const float angle[3], float distance, float result[3])
+{
+ float zsine = Sine(DegToRad(-angle[0]));
+ float zcos = Cosine(DegToRad(-angle[0]));
+
+ result[0] = Cosine(DegToRad(angle[1])) * zcos;
+ result[1] = Sine(DegToRad(angle[1])) * zcos;
+ result[2] = zsine;
+
+ ScaleVector(result, distance);
+ AddVectors(start, result, result);
+}
+
+/**
+ * Compares how close 2 floats are.
+ *
+ * @param z1 Float 1
+ * @param z2 Float 2
+ * @param tolerance How close the floats have to be to return true.
+ * @return True on success, false otherwise.
+ */
+stock bool GCIsRoughlyEqual(float z1, float z2, float tolerance)
+{
+ return FloatAbs(z1 - z2) < tolerance;
+}
+
+/**
+ * Checks if a float is within a range
+ *
+ * @param number Float to check.
+ * @param min Minimum range.
+ * @param max Maximum range.
+ * @return True on success, false otherwise.
+ */
+stock bool GCIsFloatInRange(float number, float min, float max)
+{
+ return number >= min && number <= max;
+}
+
+/**
+ * Keeps the yaw angle within the range of -180 to 180.
+ *
+ * @param angle Angle.
+ * @return Normalised angle.
+ */
+stock float GCNormaliseYaw(float angle)
+{
+ if (angle <= -180.0)
+ {
+ angle += 360.0;
+ }
+
+ if (angle > 180.0)
+ {
+ angle -= 360.0;
+ }
+
+ return angle;
+}
+
+/**
+ * Keeps the yaw angle within the range of -180 to 180.
+ *
+ * @param angle Angle.
+ * @return Normalised angle.
+ */
+stock float GCNormaliseYawRad(float angle)
+{
+ if (angle <= -FLOAT_PI)
+ {
+ angle += FLOAT_PI * 2;
+ }
+
+ if (angle > FLOAT_PI)
+ {
+ angle -= FLOAT_PI * 2;
+ }
+
+ return angle;
+}
+
+/**
+ * Linearly interpolates between 2 values.
+ *
+ * @param f1 Float 1.
+ * @param f2 Float 2.
+ * @param fraction Amount to interpolate.
+ * @return Interpolated value.
+ */
+stock float GCInterpLinear(float f1, float f2, float fraction)
+{
+ float diff = f2 - f1;
+
+ return diff * fraction + f1;
+}
+
+/**
+ * Calculates the linear fraction from a value that was interpolated and 2 values it was interpolated from.
+ *
+ * @param f1 Float 1.
+ * @param f2 Float 2.
+ * @param fraction Interpolated value.
+ * @return Fraction.
+ */
+stock float GCCalcLerpFraction(float f1, float f2, float lerped)
+{
+ float diff = f2 - f1;
+
+ float fraction = lerped - f1 / diff;
+ return fraction;
+}
+
+/**
+ * Calculate absolute value of an integer.
+ *
+ * @param x Integer.
+ * @return Absolute value of integer.
+ */
+stock int GCIntAbs(int x)
+{
+ return x >= 0 ? x : -x;
+}
+
+/**
+ * Get the maximum of 2 integers.
+ *
+ * @param n1 Integer.
+ * @param n2 Integer.
+ * @return The biggest of n1 and n2.
+ */
+stock int GCIntMax(int n1, int n2)
+{
+ return n1 > n2 ? n1 : n2;
+}
+
+/**
+ * Get the minimum of 2 integers.
+ *
+ * @param n1 Integer.
+ * @param n2 Integer.
+ * @return The smallest of n1 and n2.
+ */
+stock int GCIntMin(int n1, int n2)
+{
+ return n1 < n2 ? n1 : n2;
+}
+
+/**
+ * Checks if an integer is within a range
+ *
+ * @param number Integer to check.
+ * @param min Minimum range.
+ * @param max Maximum range.
+ * @return True on success, false otherwise.
+ */
+stock bool GCIsIntInRange(int number, int min, int max)
+{
+ return number >= min && number <= max;
+}
+
+/**
+ * Calculates a float percentage from a common fraction.
+ *
+ * @param numerator Numerator.
+ * @param denominator Denominator.
+ * @return Float percentage. -1.0 on failure.
+ */
+stock float GCCalcIntPercentage(int numerator, int denominator)
+{
+ return float(numerator) / float(denominator) * 100.0;
+}
+
+/**
+ * Integer power.
+ * Returns the base raised to the power of the exponent.
+ * Returns 0 if exponent is negative.
+ *
+ * @param base Base to be raised.
+ * @param exponent Value to raise the base.
+ * @return Value to the power of exponent.
+ */
+stock int GCIntPow(int base, int exponent)
+{
+ if (exponent < 0)
+ {
+ return 0;
+ }
+
+ int result = 1;
+ for (;;)
+ {
+ if (exponent & 1)
+ {
+ result *= base
+ }
+
+ exponent >>= 1;
+
+ if (!exponent)
+ {
+ break;
+ }
+
+ base *= base;
+ }
+ return result;
+}
+
+/**
+ * Swaps the values of 2 variables.
+ *
+ * @param cell1 Cell 1.
+ * @param cell2 Cell 2.
+ */
+stock void GCSwapCells(any &cell1, any &cell2)
+{
+ any temp = cell1;
+ cell1 = cell2;
+ cell2 = temp;
+}
+
+/**
+ * Clamps an int between min and max.
+ *
+ * @param value Float to clamp.
+ * @param min Minimum range.
+ * @param max Maximum range.
+ * @return Clamped value.
+ */
+stock int GCIntClamp(int value, int min, int max)
+{
+ if (value < min)
+ {
+ return min;
+ }
+ if (value > max)
+ {
+ return max;
+ }
+ return value;
+}
+
+/**
+ * Returns the biggest of 2 values.
+ *
+ * @param num1 Number 1.
+ * @param num2 Number 2.
+ * @return Biggest number.
+ */
+stock float GCFloatMax(float num1, float num2)
+{
+ if (num1 > num2)
+ {
+ return num1;
+ }
+ return num2;
+}
+
+/**
+ * Returns the smallest of 2 values.
+ *
+ * @param num1 Number 1.
+ * @param num2 Number 2.
+ * @return Smallest number.
+ */
+stock float GCFloatMin(float num1, float num2)
+{
+ if (num1 < num2)
+ {
+ return num1;
+ }
+ return num2;
+}
+
+/**
+ * Clamps a float between min and max.
+ *
+ * @param value Float to clamp.
+ * @param min Minimum range.
+ * @param max Maximum range.
+ * @return Clamped value.
+ */
+stock float GCFloatClamp(float value, float min, float max)
+{
+ if (value < min)
+ {
+ return min;
+ }
+ if (value > max)
+ {
+ return max;
+ }
+ return value;
+}
diff --git a/sourcemod/scripting/include/gamechaos/misc.inc b/sourcemod/scripting/include/gamechaos/misc.inc
new file mode 100644
index 0000000..f964862
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/misc.inc
@@ -0,0 +1,245 @@
+
+#if defined _gamechaos_stocks_misc_included
+ #endinput
+#endif
+#define _gamechaos_stocks_misc_included
+
+#define GC_MISC_VERSION 0x01_00_00
+#define GC_MISC_VERSION_STRING "1.0.0"
+
+/**
+ * Check if player is overlapping their MOVERIGHT and MOVELEFT buttons.
+ *
+ * @param x Buttons;
+ * @return True if overlapping, false otherwise.
+ */
+stock bool GCIsOverlapping(int buttons)
+{
+ return buttons & IN_MOVERIGHT && buttons & IN_MOVELEFT
+}
+
+/**
+ * Checks if player gained speed.
+ *
+ * @param speed Current player speed.
+ * @param lastspeed Player speed from previous tick.
+ * @return True if player gained speed, false otherwise.
+ */
+stock bool GCIsStrafeSynced(float speed, float lastspeed)
+{
+ return speed > lastspeed;
+}
+
+/**
+ * Checks if the player is not holding down their MOVERIGHT and MOVELEFT buttons.
+ *
+ * @param x Buttons.
+ * @return True if they're not holding either, false otherwise.
+ */
+stock bool GCIsDeadAirtime(int buttons)
+{
+ return !(buttons & IN_MOVERIGHT) && !(buttons & IN_MOVELEFT);
+}
+
+/**
+* Source: https://forums.alliedmods.net/showthread.php?p=2535972
+* Runs a single line of vscript code.
+* NOTE: Dont use the "script" console command, it startes a new instance and leaks memory. Use this instead!
+*
+* @param code The code to run.
+* @noreturn
+*/
+stock void GCRunScriptCode(const char[] code, any ...)
+{
+ static int scriptLogic = INVALID_ENT_REFERENCE;
+
+ if (scriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(scriptLogic))
+ {
+ scriptLogic = EntIndexToEntRef(CreateEntityByName("logic_script"));
+ if (scriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(scriptLogic))
+ {
+ SetFailState("Could not create a 'logic_script' entity.");
+ }
+
+ DispatchSpawn(scriptLogic);
+ }
+
+ char buffer[512];
+ VFormat(buffer, sizeof(buffer), code, 2);
+
+ SetVariantString(buffer);
+ AcceptEntityInput(scriptLogic, "RunScriptCode");
+}
+
+stock void GCTE_SendBeamBox(int client,
+ const float origin[3],
+ const float mins[3],
+ const float maxs[3],
+ int ModelIndex,
+ int HaloIndex = 0,
+ float Life = 3.0,
+ float Width = 2.0,
+ const int Colour[4] = { 255, 255, 255, 255 },
+ float EndWidth = 2.0,
+ int StartFrame = 0,
+ int FrameRate = 0,
+ int FadeLength = 0,
+ float Amplitude = 0.0,
+ int Speed = 0)
+{
+ // credit to some bhop timer by shavit? thanks
+ int pairs[8][3] = { { 0, 0, 0 }, { 1, 0, 0 }, { 1, 1, 0 }, { 0, 1, 0 }, { 0, 0, 1 }, { 1, 0, 1 }, { 1, 1, 1 }, { 0, 1, 1 } };
+ int edges[12][2] = { { 0, 1 }, { 0, 3 }, { 0, 4 }, { 2, 1 }, { 2, 3 }, { 2, 6 }, { 5, 4 }, { 5, 6 }, { 5, 1 }, { 7, 4 }, { 7, 6 }, { 7, 3 } };
+
+ float corners[8][3];
+ float corner[2][3];
+
+ AddVectors(origin, mins, corner[0]);
+ AddVectors(origin, maxs, corner[1]);
+
+ for (int i = 0; i < 8; i++)
+ {
+ corners[i][0] = corner[pairs[i][0]][0];
+ corners[i][1] = corner[pairs[i][1]][1];
+ corners[i][2] = corner[pairs[i][2]][2];
+ }
+
+ for (int i = 0; i < 12; i++)
+ {
+ TE_SetupBeamPoints(corners[edges[i][0]],
+ corners[edges[i][1]],
+ ModelIndex,
+ HaloIndex,
+ StartFrame,
+ FrameRate,
+ Life,
+ Width,
+ EndWidth,
+ FadeLength,
+ Amplitude,
+ Colour,
+ Speed);
+ TE_SendToClient(client);
+ }
+}
+
+stock void GCTE_SendBeamCross(int client,
+ const float origin[3],
+ int ModelIndex,
+ int HaloIndex = 0,
+ float Life = 3.0,
+ float Width = 2.0,
+ const int Colour[4] = { 255, 255, 255, 255 },
+ float EndWidth = 2.0,
+ int StartFrame = 0,
+ int FrameRate = 0,
+ int FadeLength = 0,
+ float Amplitude = 0.0,
+ int Speed = 0)
+{
+ float points[4][3];
+
+ for (int i; i < 4; i++)
+ {
+ points[i][2] = origin[2];
+ }
+
+ // -x; -y
+ points[0][0] = origin[0] - 8.0;
+ points[0][1] = origin[1] - 8.0;
+
+ // +x; -y
+ points[1][0] = origin[0] + 8.0;
+ points[1][1] = origin[1] - 8.0;
+
+ // +x; +y
+ points[2][0] = origin[0] + 8.0;
+ points[2][1] = origin[1] + 8.0;
+
+ // -x; +y
+ points[3][0] = origin[0] - 8.0;
+ points[3][1] = origin[1] + 8.0;
+
+ //draw cross
+ for (int corner; corner < 4; corner++)
+ {
+ TE_SetupBeamPoints(origin, points[corner], ModelIndex, HaloIndex, StartFrame, FrameRate, Life, Width, EndWidth, FadeLength, Amplitude, Colour, Speed);
+ TE_SendToClient(client);
+ }
+}
+
+stock void GCTE_SendBeamRectangle(int client,
+ const float origin[3],
+ const float mins[3],
+ const float maxs[3],
+ int modelIndex,
+ int haloIndex = 0,
+ float life = 3.0,
+ float width = 2.0,
+ const int colour[4] = { 255, 255, 255, 255 },
+ float endWidth = 2.0,
+ int startFrame = 0,
+ int frameRate = 0,
+ int fadeLength = 0,
+ float amplitude = 0.0,
+ int speed = 0)
+{
+ float vertices[4][3];
+ GCRectangleVerticesFromPoint(vertices, origin, mins, maxs);
+
+ // send the square
+ for (int i; i < 4; i++)
+ {
+ int j = (i == 3) ? (0) : (i + 1);
+ TE_SetupBeamPoints(vertices[i],
+ vertices[j],
+ modelIndex,
+ haloIndex,
+ startFrame,
+ frameRate,
+ life,
+ width,
+ endWidth,
+ fadeLength,
+ amplitude,
+ colour,
+ speed);
+ TE_SendToClient(client);
+ }
+}
+
+/**
+ * Calculates vertices for a rectangle from a point, mins and maxs.
+ *
+ * @param result Vertex array result.
+ * @param origin Origin to offset mins and maxs by.
+ * @param mins Minimum size of the rectangle.
+ * @param maxs Maximum size of the rectangle.
+ * @return True if overlapping, false otherwise.
+ */
+stock void GCRectangleVerticesFromPoint(float result[4][3], const float origin[3], const float mins[3], const float maxs[3])
+{
+ // Vertices are set clockwise starting from top left (-x; -y)
+
+ // -x; -y
+ result[0][0] = origin[0] + mins[0];
+ result[0][1] = origin[1] + mins[1];
+
+ // +x; -y
+ result[1][0] = origin[0] + maxs[0];
+ result[1][1] = origin[1] + mins[1];
+
+ // +x; +y
+ result[2][0] = origin[0] + maxs[0];
+ result[2][1] = origin[1] + maxs[1];
+
+ // -x; +y
+ result[3][0] = origin[0] + mins[0];
+ result[3][1] = origin[1] + maxs[1];
+
+ // z is the same for every vertex
+ for (int vertex; vertex < 4; vertex++)
+ {
+ result[vertex][2] = origin[2];
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/strings.inc b/sourcemod/scripting/include/gamechaos/strings.inc
new file mode 100644
index 0000000..8ffcb60
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/strings.inc
@@ -0,0 +1,367 @@
+
+#if defined _gamechaos_stocks_strings_included
+ #endinput
+#endif
+#define _gamechaos_stocks_strings_included
+
+// these are used for functions that return strings.
+// you can change these if they're too small/big.
+#define GC_FIXED_BUFFER_SIZE_SMALL 64
+#define GC_FIXED_BUFFER_SIZE_LARGE 4096
+
+/**
+ * Puts the values from a string of integers into an array
+ *
+ * @param string
+ * @param separator
+ * @param array
+ * @param arraysize
+ */
+stock void GCSeparateIntsFromString(const char[] string, const char[] separator, int[] array, int arraysize)
+{
+ char[][] explodedbuffer = new char[arraysize][32];
+
+ ExplodeString(string, separator, explodedbuffer, arraysize, 32);
+
+ for (int i; i < arraysize; i++)
+ {
+ array[i] = StringToInt(explodedbuffer[i]);
+ }
+}
+
+/**
+ * Prints a message to all admins in the chat area.
+ *
+ * @param format Formatting rules.
+ * @param ... Variable number of format parameters.
+ */
+stock void GCPrintToChatAdmins(const char[] format, any ...)
+{
+ char buffer[256];
+
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (GCIsValidClient(i))
+ {
+ AdminId id = GetUserAdmin(i);
+ if (!GetAdminFlag(id, Admin_Generic))
+ {
+ continue;
+ }
+ SetGlobalTransTarget(i);
+ VFormat(buffer, sizeof(buffer), format, 2);
+ PrintToChat(i, "%s", buffer);
+ }
+ }
+}
+
+/**
+ * Removes trailings zeroes from a string. Also removes the decimal point if it can.
+ *
+ * @param buffer Buffer to trim.
+ * @return Whether anything was removed.
+ */
+stock bool GCRemoveTrailing0s(char[] buffer)
+{
+ bool removed;
+ int maxlen = strlen(buffer);
+
+ if (maxlen == 0)
+ {
+ return removed;
+ }
+
+ for (int i = maxlen - 1; i > 0 && (buffer[i] == '0' || buffer[i] == '.' || buffer[i] == 0); i--)
+ {
+ if (buffer[i] == 0)
+ {
+ continue;
+ }
+ if (buffer[i] == '.')
+ {
+ buffer[i] = 0;
+ removed = true;
+ break;
+ }
+ buffer[i] = 0;
+ removed = true;
+ }
+ return removed;
+}
+
+/**
+ * Formats time by HHMMSS. Uses ticks for the time.
+ *
+ * @param timeInTicks Time in ticks.
+ * @param tickRate Tickrate.
+ * @param formattedTime String to use for formatting.
+ * @param size String size.
+ */
+stock void GCFormatTickTimeHHMMSS(int timeInTicks, float tickRate, char[] formattedTime, int size)
+{
+ if (timeInTicks <= 0)
+ {
+ FormatEx(formattedTime, size, "-00:00:00");
+ return;
+ }
+
+ int time = RoundFloat(float(timeInTicks) / tickRate * 100.0); // centiseconds
+ int iHours = time / 360000;
+ int iMinutes = time / 6000 - iHours * 6000;
+ int iSeconds = (time - iHours * 360000 - iMinutes * 6000) / 100;
+ int iCentiSeconds = time % 100;
+
+ if (iHours != 0)
+ {
+ FormatEx(formattedTime, size, "%02i:", iHours);
+ }
+ if (iMinutes != 0)
+ {
+ Format(formattedTime, size, "%s%02i:", formattedTime, iMinutes);
+ }
+
+ Format(formattedTime, size, "%s%02i.%02i", formattedTime, iSeconds, iCentiSeconds);
+}
+
+/**
+ * Formats time by HHMMSS. Uses seconds.
+ *
+ * @param seconds Time in seconds.
+ * @param formattedTime String to use for formatting.
+ * @param size String size.
+ * @param decimals Amount of decimals to use for the fractional part.
+ */
+stock void GCFormatTimeHHMMSS(float seconds, char[] formattedTime, int size, int decimals)
+{
+ int iFlooredTime = RoundToFloor(seconds);
+ int iHours = iFlooredTime / 3600;
+ int iMinutes = iFlooredTime / 60 - iHours * 60;
+ int iSeconds = iFlooredTime - iHours * 3600 - iMinutes * 60;
+ int iFraction = RoundToFloor(FloatFraction(seconds) * Pow(10.0, float(decimals)));
+
+ if (iHours != 0)
+ {
+ FormatEx(formattedTime, size, "%02i:", iHours);
+ }
+ if (iMinutes != 0)
+ {
+ Format(formattedTime, size, "%s%02i:", formattedTime, iMinutes);
+ }
+ char szFraction[32];
+ FormatEx(szFraction, sizeof(szFraction), "%i", iFraction);
+
+ int iTest = strlen(szFraction);
+ for (int i; i < decimals - iTest; i++)
+ {
+ Format(szFraction, sizeof(szFraction), "%s%s", "0", szFraction);
+ }
+
+ Format(formattedTime, size, "%s%02i.%s", formattedTime, iSeconds, szFraction);
+}
+
+/**
+ * Encodes and appends a number onto the end of a UTF-8 string.
+ *
+ * @param string String to append to.
+ * @param strsize String size.
+ * @param number Unicode codepoint to encode.
+ */
+stock void GCEncodeUtf8(char[] string, char strsize, int number)
+{
+ // UTF-8 octet sequence (only change digits marked with x)
+ /*
+ Char. number range | UTF-8 octet sequence
+ (hexadecimal) | (binary)
+ --------------------+---------------------------------------------
+ 0000 0000-0000 007F | 0xxxxxxx
+ 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
+ 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
+ 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ // byte 4 | byte 3 | byte 2 | byte 1*/
+
+ //char encodedChar = 0b_11110000_10000000_10000000_10000000;
+
+ int zeropos = strlen(string);
+
+ if (zeropos >= strsize - 1) // need one byte for null terminator
+ {
+ return;
+ }
+
+ if (number < 0)
+ {
+ //PrintToServer("ERROR: Encode() - Can't encode negative numbers");
+ return;
+ }
+
+ if (number >= 0x110_000)
+ {
+ //PrintToServer("ERROR: Encode() - Number is too big to encode");
+ return;
+ }
+
+ // 1 byte
+ if (number < 0x80)
+ {
+ string[zeropos] = number;
+ string[zeropos + 1] = '\0';
+ }
+ // 2 bytes
+ else if (number < 0x800)
+ {
+ // can't encode if we don't have enough room
+ if (zeropos + 2 >= strsize)
+ {
+ return;
+ }
+
+ string[zeropos] = 0b_1100_0000 | (number >> 6); // don't need to mask out bits over 0x7FF
+ string[zeropos + 1] = 0b_1000_0000 | (number & 0b_0011_1111);
+
+ string[zeropos + 2] = '\0';
+ }
+ // 3 bytes
+ else if (number < 0x10_000)
+ {
+ // can't encode if we don't have enough room
+ if (zeropos + 3 >= strsize)
+ {
+ return;
+ }
+
+ string[zeropos] = 0b_1110_0000 | (number >> 12); // don't need to mask out bits over 0xFFFF
+ string[zeropos + 1] = 0b_1000_0000 | ((number >> 6) & 0b_0011_1111);
+ string[zeropos + 2] = 0b_1000_0000 | (number & 0b_0011_1111);
+
+ string[zeropos + 3] = '\0';
+ }
+ // 4 bytes
+ else if (number < 0x110_000)
+ {
+ // can't encode if we don't have enough room
+ if (zeropos + 4 >= strsize)
+ {
+ return;
+ }
+
+ string[zeropos] = 0b_1111_0000 | (number >> 18); // don't need to mask out bits over 0x10FFFF
+ string[zeropos + 1] = 0b_1000_0000 | ((number >> 12) & 0b_0011_1111);
+ string[zeropos + 2] = 0b_1000_0000 | ((number >> 6) & 0b_0011_1111);
+ string[zeropos + 3] = 0b_1000_0000 | (number & 0b_0011_1111);
+
+ string[zeropos + 4] = '\0';
+ }
+}
+
+// decode a UTF-8 string into an array of unicode codepoints
+/**
+ * Decodes a UTF-8 string into an array of unicode codepoints.
+ *
+ * @param string String to decode.
+ * @param strsize String size.
+ * @param codepoints Array to use to store the codepoints.
+ * @param cplength Array length.
+ */
+stock void GCDecodeUtf8(char[] string, int strsize, int[] codepoints, int cplength)
+{
+ int charindex;
+ int cpindex;
+
+ while (charindex < strsize && cpindex < cplength)
+ {
+ if (string[charindex] == '\0')
+ {
+ break;
+ }
+
+ int bytes = GetCharBytes(string[charindex]);
+
+ switch (bytes)
+ {
+ case 1:
+ {
+ codepoints[cpindex] = string[charindex];
+ }
+ case 2:
+ {
+ codepoints[cpindex] = (string[charindex++] & 0b_0001_1111) << 6; // byte 2
+ codepoints[cpindex] |= string[charindex] & 0b_0011_1111; // byte 1
+ }
+ case 3:
+ {
+ codepoints[cpindex] = (string[charindex++] & 0b_0000_1111) << 12; // byte 3
+ codepoints[cpindex] |= (string[charindex++] & 0b_0011_1111) << 6; // byte 2
+ codepoints[cpindex] |= string[charindex] & 0b_0011_1111; // byte 1
+ }
+ case 4:
+ {
+ codepoints[cpindex] = (string[charindex++] & 0b_0000_0111) << 18; // byte 4
+ codepoints[cpindex] |= (string[charindex++] & 0b_0011_1111) << 12; // byte 3
+ codepoints[cpindex] |= (string[charindex++] & 0b_0011_1111) << 6; // byte 2
+ codepoints[cpindex] |= string[charindex] & 0b_0011_1111; // byte 1
+ }
+ }
+
+ charindex++;
+ cpindex++;
+ }
+}
+
+/**
+ * Converts an integer to a string.
+ * Same as IntToString, but it returns the string.
+ *
+ * @param num Integer to convert.
+ * @return String of the number.
+ */
+stock char[] GCIntToStringRet(int num)
+{
+ char string[GC_FIXED_BUFFER_SIZE_SMALL];
+ IntToString(num, string, sizeof string);
+ return string;
+}
+
+ /**
+ * Converts a floating point number to a string.
+ * Same as FloatToString, but it returns the string.
+ *
+ * @param num Floating point number to convert.
+ * @return String of the number.
+ */
+stock char[] GCFloatToStringRet(float num)
+{
+ char string[GC_FIXED_BUFFER_SIZE_SMALL];
+ FloatToString(num, string, sizeof string);
+ return string;
+}
+
+/**
+ * Formats a string according to the SourceMod format rules (see documentation).
+ * Same as Format, except it returns the formatted string.
+ *
+ * @param format Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @return Formatted string.
+ */
+stock char[] GCFormatReturn(const char[] format, any ...)
+{
+ char string[GC_FIXED_BUFFER_SIZE_LARGE];
+ VFormat(string, sizeof string, format, 2);
+ return string;
+}
+
+/**
+ * Removes whitespace characters from the beginning and end of a string.
+ * Same as TrimString, except it returns the formatted string and
+ * it doesn't modify the passed string.
+ *
+ * @param str The string to trim.
+ * @return Number of bytes written (UTF-8 safe).
+ */
+stock char[] GCTrimStringReturn(char[] str)
+{
+ char string[GC_FIXED_BUFFER_SIZE_LARGE];
+ strcopy(string, sizeof string, str);
+ TrimString(string);
+ return string;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/tempents.inc b/sourcemod/scripting/include/gamechaos/tempents.inc
new file mode 100644
index 0000000..7ec9b5a
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/tempents.inc
@@ -0,0 +1,62 @@
+
+#if defined _gamechaos_stocks_tempents_included
+ #endinput
+#endif
+#define _gamechaos_stocks_tempents_included
+
+// improved api of some tempents
+
+#define GC_TEMPENTS_VERSION 0x01_00_00
+#define GC_TEMPENTS_VERSION_STRING "1.0.0"
+
+#include <sdktools_tempents>
+
+/**
+ * Sets up a point to point beam effect.
+ *
+ * @param start Start position of the beam.
+ * @param end End position of the beam.
+ * @param modelIndex Precached model index.
+ * @param life Time duration of the beam.
+ * @param width Initial beam width.
+ * @param endWidth Final beam width.
+ * @param colour Color array (r, g, b, a).
+ * @param haloIndex Precached model index.
+ * @param amplitude Beam amplitude.
+ * @param speed Speed of the beam.
+ * @param fadeLength Beam fade time duration.
+ * @param frameRate Beam frame rate.
+ * @param startFrame Initial frame to render.
+ */
+stock void GCTE_SetupBeamPoints(const float start[3],
+ const float end[3],
+ int modelIndex,
+ float life = 2.0,
+ float width = 2.0,
+ float endWidth = 2.0,
+ const int colour[4] = {255, 255, 255, 255},
+ int haloIndex = 0,
+ float amplitude = 0.0,
+ int speed = 0,
+ int fadeLength = 0,
+ int frameRate = 0,
+ int startFrame = 0)
+{
+ TE_Start("BeamPoints");
+ TE_WriteVector("m_vecStartPoint", start);
+ TE_WriteVector("m_vecEndPoint", end);
+ TE_WriteNum("m_nModelIndex", modelIndex);
+ TE_WriteNum("m_nHaloIndex", haloIndex);
+ TE_WriteNum("m_nStartFrame", startFrame);
+ TE_WriteNum("m_nFrameRate", frameRate);
+ TE_WriteFloat("m_fLife", life);
+ TE_WriteFloat("m_fWidth", width);
+ TE_WriteFloat("m_fEndWidth", endWidth);
+ TE_WriteFloat("m_fAmplitude", amplitude);
+ TE_WriteNum("r", colour[0]);
+ TE_WriteNum("g", colour[1]);
+ TE_WriteNum("b", colour[2]);
+ TE_WriteNum("a", colour[3]);
+ TE_WriteNum("m_nSpeed", speed);
+ TE_WriteNum("m_nFadeLength", fadeLength);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/tracing.inc b/sourcemod/scripting/include/gamechaos/tracing.inc
new file mode 100644
index 0000000..65d54a8
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/tracing.inc
@@ -0,0 +1,242 @@
+
+#if defined _gamechaos_stocks_tracing_included
+ #endinput
+#endif
+#define _gamechaos_stocks_tracing_included
+
+#include <sdktools_trace>
+
+#define GC_TRACING_VERSION 0x01_00_00
+#define GC_TRACING_VERSION_STRING "1.0.0"
+
+/**
+ * Trace ray filter that filters players from being traced.
+ *
+ * @param entity Entity.
+ * @param data Data.
+ * @return True on success, false otherwise.
+ */
+stock bool GCTraceEntityFilterPlayer(int entity, any data)
+{
+ return entity > MAXPLAYERS;
+}
+
+/**
+ * Traces the player hull beneath the player in the direction of
+ * the player's velocity. This should be used on the tick when the player lands
+ *
+ * @param client Player's index.
+ * @param pos Player's position vector.
+ * @param velocity Player's velocity vector. This shuold have the current tick's x and y velocities, but the previous tick's z velocity, since when you're on ground, your z velocity is 0.
+ * @param result Trace endpoint on success, player's position on failure.
+ * @param bugged Whether to add gravity to the player's velocity or not.
+ * @return True on success, false otherwise.
+ */
+stock bool GCTraceLandPos(int client, const float pos[3], const float velocity[3], float result[3], float fGravity, bool bugged = false)
+{
+ float newVel[3];
+ newVel = velocity;
+
+ if (bugged)
+ {
+ // add 0.5 gravity
+ newVel[2] -= fGravity * GetTickInterval() * 0.5;
+ }
+ else
+ {
+ // add 1.5 gravity
+ newVel[2] -= fGravity * GetTickInterval() * 1.5;
+ }
+
+ ScaleVector(newVel, GetTickInterval() * 2.0);
+ float pos2[3];
+ AddVectors(pos, newVel, pos2);
+
+ float mins[3];
+ float maxs[3];
+ GetClientMins(client, mins);
+ GetClientMaxs(client, maxs);
+
+ Handle trace = TR_TraceHullFilterEx(pos, pos2, mins, maxs, MASK_PLAYERSOLID, GCTraceEntityFilterPlayer);
+
+ if (!TR_DidHit(trace))
+ {
+ result = pos;
+ CloseHandle(trace);
+ return false;
+ }
+
+ TR_GetEndPosition(result, trace);
+ CloseHandle(trace);
+
+ return true;
+}
+
+/**
+ * Traces the player hull 2 units straight down beneath the player.
+ *
+ * @param client Player's index.
+ * @param pos Player's position vector.
+ * @param result Trace endpoint on success, player's position on failure.
+ * @return True on success, false otherwise.
+ */
+stock bool GCTraceGround(int client, const float pos[3], float result[3])
+{
+ float mins[3];
+ float maxs[3];
+
+ GetClientMins(client, mins);
+ GetClientMaxs(client, maxs);
+
+ float startpos[3];
+ float endpos[3];
+
+ startpos = pos;
+ endpos = pos;
+
+ endpos[2] -= 2.0;
+
+ TR_TraceHullFilter(startpos, endpos, mins, maxs, MASK_PLAYERSOLID, GCTraceEntityFilterPlayer);
+
+ if (TR_DidHit())
+ {
+ TR_GetEndPosition(result);
+ return true;
+ }
+ else
+ {
+ result = endpos;
+ return false;
+ }
+}
+
+/**
+ * Traces a hull between 2 positions.
+ *
+ * @param pos1 Position 1.
+ * @param pos2 Position 2
+ * @param result Trace endpoint on success, player's position on failure.
+ * @return True on success, false otherwise.
+ */
+stock bool GCTraceBlock(const float pos1[3], const float pos2[3], float result[3])
+{
+ float mins[3] = {-16.0, -16.0, -1.0};
+ float maxs[3] = { 16.0, 16.0, 0.0};
+
+ TR_TraceHullFilter(pos1, pos2, mins, maxs, MASK_PLAYERSOLID, GCTraceEntityFilterPlayer);
+
+ if (TR_DidHit())
+ {
+ TR_GetEndPosition(result);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+/**
+ * Traces from player eye position in the direction of where the player is looking.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ * @return True on success, false otherwise.
+ */
+stock bool GCGetEyeRayPosition(int client, float result[3], TraceEntityFilter filter, any data = 0, int flags = MASK_PLAYERSOLID)
+{
+ float start[3];
+ float angle[3];
+
+ GetClientEyePosition(client, start);
+ GetClientEyeAngles(client, angle);
+
+ TR_TraceRayFilter(start, angle, flags, RayType_Infinite, filter, data);
+
+ if (TR_DidHit(INVALID_HANDLE))
+ {
+ TR_GetEndPosition(result, INVALID_HANDLE);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Traces from player eye position in the direction of where the player is looking, up to a certain distance.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ * @param distance Maximum distance to trace.
+ * @return True on success, false otherwise.
+ */
+stock bool GCTraceEyeRayPositionDistance(int client, float result[3], float distance)
+{
+ float start[3];
+ float angle[3];
+
+ GetClientEyePosition(client, start);
+ GetClientEyeAngles(client, angle);
+
+ float endpoint[3];
+ float zsine = Sine(DegToRad(-angle[0]));
+ float zcos = Cosine(DegToRad(-angle[0]));
+
+ endpoint[0] = Cosine(DegToRad(angle[1])) * zcos;
+ endpoint[1] = Sine(DegToRad(angle[1])) * zcos;
+ endpoint[2] = zsine;
+
+ ScaleVector(endpoint, distance);
+ AddVectors(start, endpoint, endpoint);
+
+ TR_TraceRayFilter(start, endpoint, MASK_PLAYERSOLID, RayType_EndPoint, GCTraceEntityFilterPlayer, client);
+
+ if (TR_DidHit())
+ {
+ TR_GetEndPosition(result);
+ return true;
+ }
+
+ result = endpoint;
+ return false;
+}
+
+/**
+ * Traces a hull in a certain direction and distance.
+ *
+ * @param origin Position to trace from.
+ * @param direction Trace direction.
+ * @param mins Minimum size of the hull.
+ * @param maxs Maximum size of the hull.
+ * @param result Resultant vector.
+ * @return True on success, false otherwise.
+ */
+stock bool GCTraceHullDirection(const float origin[3],
+ const float direction[3],
+ const float mins[3],
+ const float maxs[3],
+ float result[3],
+ float distance,
+ TraceEntityFilter filter,
+ any data = 0,
+ int flags = MASK_PLAYERSOLID)
+{
+ float pos2[3];
+ float zsine = Sine(DegToRad(-direction[0]));
+ float zcos = Cosine(DegToRad(-direction[0]));
+
+ pos2[0] = Cosine(DegToRad(direction[1])) * zcos;
+ pos2[1] = Sine(DegToRad(direction[1])) * zcos;
+ pos2[2] = zsine;
+
+ ScaleVector(pos2, distance);
+ AddVectors(origin, pos2, pos2);
+
+ TR_TraceHullFilter(origin, pos2, mins, maxs, flags, filter, data);
+ if (TR_DidHit())
+ {
+ TR_GetEndPosition(result);
+ return true;
+ }
+ result = pos2;
+ return false;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gamechaos/vectors.inc b/sourcemod/scripting/include/gamechaos/vectors.inc
new file mode 100644
index 0000000..79d5e8f
--- /dev/null
+++ b/sourcemod/scripting/include/gamechaos/vectors.inc
@@ -0,0 +1,66 @@
+
+#if defined _gamechaos_stocks_vectors_included
+ #endinput
+#endif
+#define _gamechaos_stocks_vectors_included
+
+#define GC_VECTORS_VERSION 0x01_00_01
+#define GC_VECTORS_VERSION_STRING "1.0.1"
+
+/**
+ * Calculates the horizontal (x, y) length of a vector.
+ *
+ * @param vec Vector.
+ * @return Vector length (magnitude).
+ */
+stock float GCGetVectorLength2D(const float vec[3])
+{
+ float tempVec[3];
+ tempVec = vec;
+ tempVec[2] = 0.0;
+
+ return GetVectorLength(tempVec);
+}
+
+/**
+ * Calculates the horizontal (x, y) distance between 2 vectors.
+ *
+ * @param x Vector 1.
+ * @param y Vector 2.
+ * @param tolerance How close the floats have to be to return true.
+ * @return True on success, false otherwise.
+ */
+stock float GCGetVectorDistance2D(const float x[3], const float y[3])
+{
+ float x2[3];
+ float y2[3];
+
+ x2 = x;
+ y2 = y;
+
+ x2[2] = 0.0;
+ y2[2] = 0.0;
+
+ return GetVectorDistance(x2, y2);
+}
+
+/**
+ * Checks if 2 vectors are exactly equal.
+ *
+ * @param a Vector 1.
+ * @param b Vector 2.
+ * @return True on success, false otherwise.
+ */
+stock bool GCVectorsEqual(const float a[3], const float b[3])
+{
+ bool result = true;
+ for (int i = 0; i < 3; i++)
+ {
+ if (a[i] != b[i])
+ {
+ result = false;
+ break;
+ }
+ }
+ return result;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/glib/addressutils.inc b/sourcemod/scripting/include/glib/addressutils.inc
new file mode 100644
index 0000000..bbe8f14
--- /dev/null
+++ b/sourcemod/scripting/include/glib/addressutils.inc
@@ -0,0 +1,54 @@
+#if defined _addressutils_included
+#endinput
+#endif
+#define _addressutils_included
+
+methodmap AddressBase
+{
+ property Address Address
+ {
+ public get() { return view_as<Address>(this); }
+ }
+}
+
+//-==Operator overloadings
+stock Address operator+(Address l, int r)
+{
+ return l + view_as<Address>(r);
+}
+
+stock Address operator+(int l, Address r)
+{
+ return view_as<Address>(l) + r;
+}
+
+stock Address operator-(Address l, int r)
+{
+ return l - view_as<Address>(r);
+}
+
+stock Address operator-(int l, Address r)
+{
+ return view_as<Address>(l) - r;
+}
+
+stock Address operator*(Address l, int r)
+{
+ return l * view_as<Address>(r);
+}
+
+stock Address operator*(int l, Address r)
+{
+ return view_as<Address>(l) * r;
+}
+
+stock Address operator/(Address l, int r)
+{
+ return l / view_as<Address>(r);
+}
+
+stock Address operator/(int l, Address r)
+{
+ return view_as<Address>(l) / r;
+}
+//Operator overloadings==- \ No newline at end of file
diff --git a/sourcemod/scripting/include/glib/assertutils.inc b/sourcemod/scripting/include/glib/assertutils.inc
new file mode 100644
index 0000000..83cd90d
--- /dev/null
+++ b/sourcemod/scripting/include/glib/assertutils.inc
@@ -0,0 +1,61 @@
+#if defined _assertutils_included
+#endinput
+#endif
+#define _assertutils_included
+
+/* Compile time settings for this include. Should be defined before including this file.
+* #define ASSERTUTILS_DISABLE //Disables all assertions
+* #define ASSERTUTILS_FAILSTATE_FUNC //Define the name of the function that should be called when assertion is hit
+*/
+
+#if !defined SNAME
+#define __SNAME ""
+#else
+#define __SNAME SNAME
+#endif
+
+#define ASSERT_FMT_STRING_LEN 512
+
+#if defined ASSERTUTILS_DISABLE
+
+#define ASSERT(%1)%2;
+#define ASSERT_MSG(%1,%2)%3;
+#define ASSERT_FMT(%1,%2)%3;
+#define ASSERT_FINAL(%1)%2;
+#define ASSERT_FINAL_MSG(%1,%2)%3;
+
+#elseif defined ASSERTUTILS_FAILSTATE_FUNC
+
+#define ASSERT(%1) if(!(%1)) ASSERTUTILS_FAILSTATE_FUNC(__SNAME..."Assertion failed: \""...#%1..."\"")
+#define ASSERT_MSG(%1,%2) if(!(%1)) ASSERTUTILS_FAILSTATE_FUNC(__SNAME...%2)
+#define ASSERT_FMT(%1,%2) if(!(%1)) ASSERTUTILS_FAILSTATE_FUNC(__SNAME...%2)
+#define ASSERT_FINAL(%1) if(!(%1)) SetFailState(__SNAME..."Assertion failed: \""...#%1..."\"")
+#define ASSERT_FINAL_MSG(%1,%2) if(!(%1)) SetFailState(__SNAME...%2)
+
+#else
+
+#define ASSERT(%1) if(!(%1)) SetFailState(__SNAME..."Assertion failed: \""...#%1..."\"")
+#define ASSERT_MSG(%1,%2) if(!(%1)) SetFailState(__SNAME...%2)
+#define ASSERT_FMT(%1,%2) if(!(%1)) SetFailState(__SNAME...%2)
+#define ASSERT_FINAL(%1) ASSERT(%1)
+#define ASSERT_FINAL_MSG(%1,%2) ASSERT_MSG(%1,%2)
+
+#endif
+
+// Might be redundant as default ASSERT_MSG accept format arguments just fine.
+#if 0
+stock void ASSERT_FMT(bool result, char[] fmt, any ...)
+{
+#if !defined ASSERTUTILS_DISABLE
+ if(!result)
+ {
+ char buff[ASSERT_FMT_STRING_LEN];
+ VFormat(buff, sizeof(buff), fmt, 3);
+
+ SetFailState(__SNAME..."%s", buff);
+ }
+#endif
+}
+#endif
+
+#undef ASSERT_FMT_STRING_LEN \ No newline at end of file
diff --git a/sourcemod/scripting/include/glib/memutils.inc b/sourcemod/scripting/include/glib/memutils.inc
new file mode 100644
index 0000000..5813d92
--- /dev/null
+++ b/sourcemod/scripting/include/glib/memutils.inc
@@ -0,0 +1,232 @@
+#if defined _memutils_included
+#endinput
+#endif
+#define _memutils_included
+
+#include "glib/assertutils"
+#include "glib/addressutils"
+
+/* Compile time settings for this include. Should be defined before including this file.
+* #define MEMUTILS_PLUGINENDCALL //This should be defined if main plugin has OnPluginEnd() forward used.
+*/
+
+#define MEM_LEN_SAFE_THRESHOLD 2000
+
+//-==PatchHandling methodmap
+static StringMap gPatchStack;
+
+methodmap PatchHandler < AddressBase
+{
+ public PatchHandler(Address addr)
+ {
+ ASSERT(addr != Address_Null);
+
+ if(!gPatchStack)
+ gPatchStack = new StringMap();
+
+ return view_as<PatchHandler>(addr);
+ }
+
+ property any Any
+ {
+ public get() { return view_as<any>(this); }
+ }
+
+ public void Save(int len)
+ {
+ ASSERT(gPatchStack);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+
+ len++;
+
+ int[] arr = new int[len];
+ arr[0] = len;
+
+ for(int i = 0; i < len - 1; i++)
+ arr[i + 1] = LoadFromAddress(this.Address + i, NumberType_Int8);
+
+ char buff[32];
+ IntToString(this.Any, buff, sizeof(buff));
+ gPatchStack.SetArray(buff, arr, len);
+ }
+
+ public void PatchNSave(int len, char byte = 0x90)
+ {
+ ASSERT(gPatchStack);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+
+ len++;
+
+ int[] arr = new int[len];
+ arr[0] = len;
+
+ for(int i = 0; i < len - 1; i++)
+ {
+ arr[i + 1] = LoadFromAddress(this.Address + i, NumberType_Int8);
+ StoreToAddress(this.Address + i, byte, NumberType_Int8);
+ }
+
+ char buff[32];
+ IntToString(this.Any, buff, sizeof(buff));
+ gPatchStack.SetArray(buff, arr, len);
+ }
+
+ public void PatchNSaveSeq(const char[] data, int len)
+ {
+ ASSERT(gPatchStack);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+
+ len++;
+
+ int[] arr = new int[len];
+ arr[0] = len;
+
+ for(int i = 0; i < len - 1; i++)
+ {
+ arr[i + 1] = LoadFromAddress(this.Address + i, NumberType_Int8);
+ StoreToAddress(this.Address + i, data[i], NumberType_Int8);
+ }
+
+ char buff[32];
+ IntToString(this.Any, buff, sizeof(buff));
+ gPatchStack.SetArray(buff, arr, len);
+ }
+
+ public void Restore()
+ {
+ if(!gPatchStack)
+ return;
+
+ char buff[32];
+ IntToString(this.Any, buff, sizeof(buff));
+
+ int arrSize[1];
+ if(!gPatchStack.GetArray(buff, arrSize, sizeof(arrSize)))
+ return;
+
+ int[] arr = new int[arrSize[0]];
+ gPatchStack.GetArray(buff, arr, arrSize[0]);
+ gPatchStack.Remove(buff);
+
+ for(int i = 0; i < arrSize[0] - 1; i++)
+ StoreToAddress(this.Address + i, arr[i + 1], NumberType_Int8);
+
+ if(gPatchStack.Size == 0)
+ delete gPatchStack;
+ }
+}
+
+public void OnPluginEnd()
+{
+ if(gPatchStack)
+ {
+ StringMapSnapshot sms = gPatchStack.Snapshot();
+ char buff[32];
+ Address addr;
+
+ for(int i = 0; i < sms.Length; i++)
+ {
+ sms.GetKey(i, buff, sizeof(buff));
+ addr = view_as<Address>(StringToInt(buff));
+ view_as<PatchHandler>(addr).Restore();
+ }
+ }
+
+#if defined MEMUTILS_PLUGINENDCALL
+ OnPluginEnd_MemUtilsRedefined();
+#endif
+}
+#undef OnPluginEnd
+#if defined MEMUTILS_PLUGINENDCALL
+#define OnPluginEnd OnPluginEnd_MemUtilsRedefined
+#else
+#define OnPluginEnd OnPluginEnd_Redifined(){}\
+void MEMUTILS_INCLUDE_WARNING_OnPluginEnd_REDIFINITION
+#endif
+//PatchHandling methodmap==-
+
+//-==Other util functions
+stock int LoadStringFromAddress(Address addr, char[] buff, int length)
+{
+ int i;
+ for(i = 0; i < length && (buff[i] = LoadFromAddress(addr + i, NumberType_Int8)) != '\0'; i++) { }
+ buff[i == length ? i - 1 : i] = '\0';
+ return i;
+}
+
+stock void DumpOnAddress(Address addr, int len, int columns = 10)
+{
+ char buff[128], buff2[128];
+
+ ASSERT(addr != Address_Null);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+
+ Format(buff, sizeof(buff), "[0x%08X]", addr);
+ char chr;
+ for(int i = 0; i < len; i++)
+ {
+ chr = LoadFromAddress(addr + i, NumberType_Int8);
+ Format(buff, sizeof(buff), "%s %02X", buff, chr);
+ Format(buff2, sizeof(buff2), "%s%c", buff2, (chr > ' ' && chr != 0x7F && chr != 0xFF ? chr : '.'));
+ if(i % columns == columns - 1)
+ {
+ PrintToServer(__SNAME..."%s %s", buff, buff2);
+ Format(buff, sizeof(buff), "[0x%08X]", addr + i);
+ buff2[0] = '\0';
+ }
+ }
+
+ if((len - 1) % columns != columns - 1)
+ PrintToServer(__SNAME..."%s %s", buff, buff2);
+}
+
+//NO OVERLAPPING!!
+stock void MoveBytes(Address from, Address to, int len, char replace_with = 0x90)
+{
+ ASSERT(from != Address_Null);
+ ASSERT(to != Address_Null);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+ ASSERT(to < from || to > from + len);
+
+ if(from == to)
+ return;
+
+ for(int i = 0; i < len; i++)
+ {
+ StoreToAddress(to + i, LoadFromAddress(from + i, NumberType_Int8), NumberType_Int8);
+ StoreToAddress(from + i, replace_with, NumberType_Int8);
+ }
+}
+
+stock void CutNCopyBytes(Address from, Address to, int len, char replace_with = 0x90)
+{
+ ASSERT(from != Address_Null);
+ ASSERT(to != Address_Null);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+
+ if(from == to)
+ return;
+
+ int[] arr = new int[len];
+
+ for(int i = 0; i < len; i++)
+ {
+ arr[i] = LoadFromAddress(from + i, NumberType_Int8);
+ StoreToAddress(from + i, replace_with, NumberType_Int8);
+ }
+
+ for(int i = 0; i < len; i++)
+ StoreToAddress(to + i, arr[i], NumberType_Int8);
+}
+
+stock void PatchArea(Address addr, int len, char byte = 0x90)
+{
+ ASSERT(addr != Address_Null);
+ ASSERT(len > 0 && len < MEM_LEN_SAFE_THRESHOLD);
+
+ for(int i = 0; i < len; i++)
+ StoreToAddress(addr + i, byte, NumberType_Int8);
+}
+//Other util functions==-
+
+#undef MEM_LEN_SAFE_THRESHOLD \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz.inc b/sourcemod/scripting/include/gokz.inc
new file mode 100644
index 0000000..edbd896
--- /dev/null
+++ b/sourcemod/scripting/include/gokz.inc
@@ -0,0 +1,1097 @@
+/*
+ GOKZ General Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_included_
+#endinput
+#endif
+#define _gokz_included_
+#include <cstrike>
+#include <movement>
+
+#include <gokz/version>
+
+
+
+// =====[ ENUMS ]=====
+
+enum ObsMode
+{
+ ObsMode_None = 0, // Not in spectator mode
+ ObsMode_DeathCam, // Special mode for death cam animation
+ ObsMode_FreezeCam, // Zooms to a target, and freeze-frames on them
+ ObsMode_Fixed, // View from a fixed camera position
+ ObsMode_InEye, // Follow a player in first person view
+ ObsMode_Chase, // Follow a player in third person view
+ ObsMode_Roaming // Free roaming
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define GOKZ_SOURCE_URL "https://github.com/KZGlobalTeam/gokz"
+#define GOKZ_UPDATER_BASE_URL "http://updater.gokz.org/v2/"
+#define GOKZ_COLLISION_GROUP_STANDARD 2
+#define GOKZ_COLLISION_GROUP_NOTRIGGER 1
+#define GOKZ_TP_FREEZE_TICKS 5
+#define EPSILON 0.000001
+#define PI 3.14159265359
+#define SPEED_NORMAL 250.0
+#define SPEED_NO_WEAPON 260.0
+#define FLOAT_MAX view_as<float>(0x7F7FFFFF)
+#define SF_BUTTON_USE_ACTIVATES 1024
+#define IGNORE_JUMP_TIME 0.2
+stock float PLAYER_MINS[3] = {-16.0, -16.0, 0.0};
+stock float PLAYER_MAXS[3] = {16.0, 16.0, 72.0};
+stock float PLAYER_MAXS_DUCKED[3] = {16.0, 16.0, 54.0};
+
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Represents a time float as a string e.g. 01:23.45.
+ *
+ * @param time Time in seconds.
+ * @param precise Whether to include fractional seconds.
+ * @return String representation of time.
+ */
+stock char[] GOKZ_FormatTime(float time, bool precise = true)
+{
+ char formattedTime[12];
+
+ int roundedTime = RoundFloat(time * 100); // Time rounded to number of centiseconds
+
+ int centiseconds = roundedTime % 100;
+ roundedTime = (roundedTime - centiseconds) / 100;
+ int seconds = roundedTime % 60;
+ roundedTime = (roundedTime - seconds) / 60;
+ int minutes = roundedTime % 60;
+ roundedTime = (roundedTime - minutes) / 60;
+ int hours = roundedTime;
+
+ if (hours == 0)
+ {
+ if (precise)
+ {
+ FormatEx(formattedTime, sizeof(formattedTime), "%02d:%02d.%02d", minutes, seconds, centiseconds);
+ }
+ else
+ {
+ FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d", minutes, seconds);
+ }
+ }
+ else
+ {
+ if (precise)
+ {
+ FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d.%02d", hours, minutes, seconds, centiseconds);
+ }
+ else
+ {
+ FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d", hours, minutes, seconds);
+ }
+ }
+ return formattedTime;
+}
+
+/**
+ * Checks if the value is a valid client entity index, if they are in-game and not GOTV.
+ *
+ * @param client Client index.
+ * @return Whether client is valid.
+ */
+stock bool IsValidClient(int client)
+{
+ return client >= 1 && client <= MaxClients && IsClientInGame(client) && !IsClientSourceTV(client);
+}
+
+/**
+ * Returns the greater of two float values.
+ *
+ * @param value1 First value.
+ * @param value2 Second value.
+ * @return Greatest value.
+ */
+stock float FloatMax(float value1, float value2)
+{
+ if (value1 >= value2)
+ {
+ return value1;
+ }
+ return value2;
+}
+
+/**
+ * Returns the lesser of two float values.
+ *
+ * @param value1 First value.
+ * @param value2 Second value.
+ * @return Lesser value.
+ */
+stock float FloatMin(float value1, float value2)
+{
+ if (value1 <= value2)
+ {
+ return value1;
+ }
+ return value2;
+}
+
+/**
+ * Clamp a float value between an upper and lower bound.
+ *
+ * @param value Preferred value.
+ * @param min Minimum value.
+ * @param max Maximum value.
+ * @return The closest value to the preferred value.
+ */
+stock float FloatClamp(float value, float min, float max)
+{
+ if (value >= max)
+ {
+ return max;
+ }
+ if (value <= min)
+ {
+ return min;
+ }
+ return value;
+}
+
+
+/**
+ * Returns the greater of two int values.
+ *
+ * @param value1 First value.
+ * @param value2 Second value.
+ * @return Greatest value.
+ */
+stock int IntMax(int value1, int value2)
+{
+ if (value1 >= value2)
+ {
+ return value1;
+ }
+ return value2;
+}
+
+/**
+ * Returns the lesser of two int values.
+ *
+ * @param value1 First value.
+ * @param value2 Second value.
+ * @return Lesser value.
+ */
+stock int IntMin(int value1, int value2)
+{
+ if (value1 <= value2)
+ {
+ return value1;
+ }
+ return value2;
+}
+
+/**
+ * Rounds a float to the nearest specified power of 10.
+ *
+ * @param value Value to round.
+ * @param power Power of 10 to round to.
+ * @return Rounded value.
+ */
+stock float RoundToPowerOfTen(float value, int power)
+{
+ float pow = Pow(10.0, float(power));
+ return RoundFloat(value / pow) * pow;
+}
+
+/**
+ * Sets all characters in a string to lower case.
+ *
+ * @param input Input string.
+ * @param output Output buffer.
+ * @param size Maximum size of output.
+ */
+stock void String_ToLower(const char[] input, char[] output, int size)
+{
+ size--;
+ int i = 0;
+ while (input[i] != '\0' && i < size)
+ {
+ output[i] = CharToLower(input[i]);
+ i++;
+ }
+ output[i] = '\0';
+}
+
+/**
+ * Gets the client's observer mode.
+ *
+ * @param client Client index.
+ * @return Current observer mode.
+ */
+stock ObsMode GetObserverMode(int client)
+{
+ return view_as<ObsMode>(GetEntProp(client, Prop_Send, "m_iObserverMode"));
+}
+
+/**
+ * Gets the player a client is spectating.
+ *
+ * @param client Client index.
+ * @return Client index of target, or -1 if not spectating anyone.
+ */
+stock int GetObserverTarget(int client)
+{
+ if (!IsValidClient(client))
+ {
+ return -1;
+ }
+ ObsMode mode = GetObserverMode(client);
+ if (mode == ObsMode_InEye || mode == ObsMode_Chase)
+ {
+ return GetEntPropEnt(client, Prop_Send, "m_hObserverTarget");
+ }
+ return -1;
+}
+
+/**
+ * Emits a sound to other players that are spectating the client.
+ *
+ * @param client Client being spectated.
+ * @param sound Sound to play.
+ */
+stock void EmitSoundToClientSpectators(int client, const char[] sound)
+{
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (IsValidClient(i) && GetObserverTarget(i) == client)
+ {
+ EmitSoundToClient(i, sound);
+ }
+ }
+}
+
+/**
+ * Calculates the lowest angle from angle A to angle B.
+ * Input and result angles are between -180 and 180.
+ *
+ * @param angleA Angle A.
+ * @param angleB Angle B.
+ * @return Delta angle.
+ */
+stock float CalcDeltaAngle(float angleA, float angleB)
+{
+ float difference = angleB - angleA;
+
+ if (difference > 180.0)
+ {
+ difference = difference - 360.0;
+ }
+ else if (difference <= -180.0)
+ {
+ difference = difference + 360.0;
+ }
+
+ return difference;
+}
+
+/**
+ * Strips all color control characters in a string.
+ * The Output buffer can be the same as the input buffer.
+ * Original code by Psychonic, thanks.
+ * Source: smlib
+ *
+ * @param input Input String.
+ * @param output Output String.
+ * @param size Max Size of the Output string
+ */
+stock void Color_StripFromChatText(const char[] input, char[] output, int size)
+{
+ int x = 0;
+ for (int i = 0; input[i] != '\0'; i++) {
+
+ if (x + 1 == size)
+ {
+ break;
+ }
+
+ int character = input[i];
+
+ if (character > 0x08)
+ {
+ output[x++] = character;
+ }
+ }
+
+ output[x] = '\0';
+}
+
+/**
+ * Returns an integer as a string.
+ *
+ * @param num Integer to stringify.
+ * @return Integer as a string.
+ */
+stock char[] IntToStringEx(int num)
+{
+ char string[12];
+ IntToString(num, string, sizeof(string));
+ return string;
+}
+
+/**
+ * Returns a float as a string.
+ *
+ * @param num Float to stringify.
+ * @return Float as a string.
+ */
+stock char[] FloatToStringEx(float num)
+{
+ char string[32];
+ FloatToString(num, string, sizeof(string));
+ return string;
+}
+
+/**
+ * Increment an index, looping back to 0 if the max value is reached.
+ *
+ * @param index Current index.
+ * @param buffer Max value of index.
+ * @return Current index incremented, or 0 if max value is reached.
+ */
+stock int NextIndex(int index, int max)
+{
+ index++;
+ if (index == max)
+ {
+ return 0;
+ }
+ return index;
+}
+
+/**
+ * Reorders an array with current index at the front, and previous
+ * values after, including looping back to the end after reaching
+ * the start of the array.
+ *
+ * @param input Array to reorder.
+ * @param inputSize Size of input array.
+ * @param buffer Output buffer.
+ * @param bufferSize Size of buffer.
+ * @param index Index of current/most recent value of input array.
+ */
+stock void SortByRecent(const int[] input, int inputSize, int[] buffer, int bufferSize, int index)
+{
+ int reorderedIndex = 0;
+ for (int i = index; reorderedIndex < bufferSize && i >= 0; i--)
+ {
+ buffer[reorderedIndex] = input[i];
+ reorderedIndex++;
+ }
+ for (int i = inputSize - 1; reorderedIndex < bufferSize && i > index; i--)
+ {
+ buffer[reorderedIndex] = input[i];
+ reorderedIndex++;
+ }
+}
+
+/**
+ * Returns the Steam account ID for a given SteamID2.
+ * Checks for invalid input are not very extensive.
+ *
+ * @param steamID2 SteamID2 to convert.
+ * @return Steam account ID, or -1 if invalid.
+ */
+stock int Steam2ToSteamAccountID(const char[] steamID2)
+{
+ char pieces[3][16];
+ if (ExplodeString(steamID2, ":", pieces, sizeof(pieces), sizeof(pieces[])) != 3)
+ {
+ return -1;
+ }
+
+ int IDNumberPart1 = StringToInt(pieces[1]);
+ int IDNumberPart2 = StringToInt(pieces[2]);
+ if (pieces[1][0] != '0' && IDNumberPart1 == 0 || IDNumberPart1 != 0 && IDNumberPart1 != 1 || IDNumberPart2 <= 0)
+ {
+ return -1;
+ }
+
+ return IDNumberPart1 + (IDNumberPart2 << 1);
+}
+
+/**
+ * Teleports a player and removes their velocity and base velocity
+ * immediately and also every tick for the next 5 ticks. Automatically
+ * makes the player crouch if there is a ceiling above them.
+ *
+ * @param client Client index.
+ * @param origin Origin to teleport to.
+ * @param angles Eye angles to set.
+ */
+stock void TeleportPlayer(int client, const float origin[3], const float angles[3], bool setAngles = true, bool holdStill = true)
+{
+ // Clear the player's parent before teleporting to fix being
+ // teleported into seemingly random places if the player has a parent.
+ AcceptEntityInput(client, "ClearParent");
+
+ Movement_SetOrigin(client, origin);
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ Movement_SetBaseVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ if (setAngles)
+ {
+ // NOTE: changing angles with TeleportEntity can fail due to packet loss!!!
+ // (Movement_SetEyeAngles is a thin wrapper of TeleportEntity)
+ Movement_SetEyeAngles(client, angles);
+ }
+ // Duck the player if there is something blocking them from above
+ Handle trace = TR_TraceHullFilterEx(origin,
+ origin,
+ view_as<float>( { -16.0, -16.0, 0.0 } ), // Standing players are 32 x 32 x 72
+ view_as<float>( { 16.0, 16.0, 72.0 } ),
+ MASK_PLAYERSOLID,
+ TraceEntityFilterPlayers,
+ client);
+ bool ducked = TR_DidHit(trace);
+
+ if (holdStill)
+ {
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Intelligently hold player still to prevent booster and trigger exploits
+ StartHoldStill(client, ducked);
+ }
+ else if (ducked)
+ {
+ ForcePlayerDuck(client);
+ }
+
+ delete trace;
+}
+
+static void StartHoldStill(int client, bool ducked)
+{
+ DataPack data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(0); // tick counter
+ data.WriteCell(GOKZ_TP_FREEZE_TICKS); // number of ticks to hold still
+ data.WriteCell(ducked);
+ ContinueHoldStill(data);
+}
+
+public void ContinueHoldStill(DataPack data)
+{
+ data.Reset();
+ int client = GetClientOfUserId(data.ReadCell());
+ int ticks = data.ReadCell();
+ int tickCount = data.ReadCell();
+ bool ducked = data.ReadCell();
+ delete data;
+
+ if (!IsValidClient(client))
+ {
+ return;
+ }
+
+ if (ticks < tickCount)
+ {
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ Movement_SetBaseVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ Movement_SetGravity(client, 1.0);
+
+ // Don't drop the player off of ladders.
+ // The game will automatically change the movetype back to MOVETYPE_WALK if it can't find a ladder.
+ // Don't change the movetype if it's currently MOVETYPE_NONE, as that means the player is paused.
+ if (Movement_GetMovetype(client) != MOVETYPE_NONE)
+ {
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Force duck on player and make sure that the player can't trigger triggers above them.
+ // they can still trigger triggers even when we force ducking.
+ if (ducked)
+ {
+ ForcePlayerDuck(client);
+
+ if (ticks < tickCount - 1)
+ {
+ // Don't trigger triggers
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER);
+ }
+ else
+ {
+ // Let the player trigger triggers on the last tick
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+ }
+ }
+
+ ++ticks;
+ data = new DataPack();
+ data.WriteCell(GetClientUserId(client));
+ data.WriteCell(ticks);
+ data.WriteCell(tickCount);
+ data.WriteCell(ducked);
+ RequestFrame(ContinueHoldStill, data);
+ }
+}
+
+/**
+ * Forces the player to instantly duck.
+ *
+ * @param client Client index.
+ */
+stock void ForcePlayerDuck(int client)
+{
+ // these are both necessary, because on their own the player will sometimes still be in a state that isn't fully ducked.
+ SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", 1.0, 0);
+ SetEntProp(client, Prop_Send, "m_bDucking", false);
+ SetEntProp(client, Prop_Send, "m_bDucked", true);
+}
+
+/**
+ * Returns whether the player is stuck e.g. in a wall after noclipping.
+ *
+ * @param client Client index.
+ * @return Whether player is stuck.
+ */
+stock bool IsPlayerStuck(int client)
+{
+ float vecMin[3], vecMax[3], vecOrigin[3];
+
+ GetClientMins(client, vecMin);
+ GetClientMaxs(client, vecMax);
+ GetClientAbsOrigin(client, vecOrigin);
+
+ TR_TraceHullFilter(vecOrigin, vecOrigin, vecMin, vecMax, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+ return TR_DidHit(); // head in wall ?
+}
+
+/**
+ * Retrieves the absolute origin of an entity.
+ *
+ * @param entity Index of the entity.
+ * @param result Entity's origin if successful.
+ * @return Returns true if successful.
+ */
+stock bool GetEntityAbsOrigin(int entity, float result[3])
+{
+ if (!IsValidEntity(entity))
+ {
+ return false;
+ }
+
+ if (!HasEntProp(entity, Prop_Data, "m_vecAbsOrigin"))
+ {
+ return false;
+ }
+
+ GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", result);
+ return true;
+}
+
+/**
+ * Retrieves the name of an entity.
+ *
+ * @param entity Index of the entity.
+ * @param buffer Buffer to store the name.
+ * @param maxlength Maximum length of the buffer.
+ * @return Number of non-null bytes written.
+ */
+stock int GetEntityName(int entity, char[] buffer, int maxlength)
+{
+ return GetEntPropString(entity, Prop_Data, "m_iName", buffer, maxlength);
+}
+
+/**
+ * Finds an entity by name or by name and classname.
+ * Taken from smlib https://github.com/bcserv/smlib
+ * This can take anywhere from ~0.2% to ~11% of frametime (i5-7600k) in the worst case scenario where
+ * every entity which has a name (4096 of them) is iterated over. Your mileage may vary.
+ *
+ * @param name Name of the entity to find.
+ * @param className Optional classname to match along with name.
+ * @param ignorePlayers Ignore player entities.
+ * @return Entity index if successful, INVALID_ENT_REFERENCE if not.
+ */
+stock int GOKZFindEntityByName(const char[] name, const char[] className = "", bool ignorePlayers = false)
+{
+ int result = INVALID_ENT_REFERENCE;
+ if (className[0] == '\0')
+ {
+ // HACK: Double the limit to get non-networked entities too.
+ // https://developer.valvesoftware.com/wiki/Entity_limit
+ int realMaxEntities = GetMaxEntities() * 2;
+ int startEntity = 1;
+ if (ignorePlayers)
+ {
+ startEntity = MaxClients + 1;
+ }
+ for (int entity = startEntity; entity < realMaxEntities; entity++)
+ {
+ if (!IsValidEntity(entity))
+ {
+ continue;
+ }
+
+ char entName[65];
+ GetEntityName(entity, entName, sizeof(entName));
+ if (StrEqual(entName, name))
+ {
+ result = entity;
+ break;
+ }
+ }
+ }
+ else
+ {
+ int entity = INVALID_ENT_REFERENCE;
+ while ((entity = FindEntityByClassname(entity, className)) != INVALID_ENT_REFERENCE)
+ {
+ char entName[65];
+ GetEntityName(entity, entName, sizeof(entName));
+ if (StrEqual(entName, name))
+ {
+ result = entity;
+ break;
+ }
+ }
+ }
+ return result;
+}
+
+/**
+ * Gets the current map's display name in lower case.
+ *
+ * @param buffer Buffer to store the map name.
+ * @param maxlength Maximum length of buffer.
+ */
+stock void GetCurrentMapDisplayName(char[] buffer, int maxlength)
+{
+ char map[PLATFORM_MAX_PATH];
+ GetCurrentMap(map, sizeof(map));
+ GetMapDisplayName(map, map, sizeof(map));
+ String_ToLower(map, buffer, maxlength);
+}
+
+/**
+ * Gets the current map's file size.
+ */
+stock int GetCurrentMapFileSize()
+{
+ char mapBuffer[PLATFORM_MAX_PATH];
+ GetCurrentMap(mapBuffer, sizeof(mapBuffer));
+ Format(mapBuffer, sizeof(mapBuffer), "maps/%s.bsp", mapBuffer);
+ return FileSize(mapBuffer);
+}
+
+/**
+ * Copies the elements of a source vector to a destination vector.
+ *
+ * @param src Source vector.
+ * @param dest Destination vector.
+ */
+stock void CopyVector(const any src[3], any dest[3])
+{
+ dest[0] = src[0];
+ dest[1] = src[1];
+ dest[2] = src[2];
+}
+
+/**
+ * Returns whether the player is spectating.
+ *
+ * @param client Client index.
+ */
+stock bool IsSpectating(int client)
+{
+ int team = GetClientTeam(client);
+ return team == CS_TEAM_SPECTATOR || team == CS_TEAM_NONE;
+}
+
+/**
+ * Rotate a vector on an axis.
+ *
+ * @param vec Vector to rotate.
+ * @param axis Axis to rotate around.
+ * @param theta Angle in radians.
+ * @param result Rotated vector.
+ */
+stock void RotateVectorAxis(float vec[3], float axis[3], float theta, float result[3])
+{
+ float cosTheta = Cosine(theta);
+ float sinTheta = Sine(theta);
+
+ float axisVecCross[3];
+ GetVectorCrossProduct(axis, vec, axisVecCross);
+
+ for (int i = 0; i < 3; i++)
+ {
+ result[i] = (vec[i] * cosTheta) + (axisVecCross[i] * sinTheta) + (axis[i] * GetVectorDotProduct(axis, vec)) * (1.0 - cosTheta);
+ }
+}
+
+/**
+ * Rotate a vector by pitch and yaw.
+ *
+ * @param vec Vector to rotate.
+ * @param pitch Pitch angle (in degrees).
+ * @param yaw Yaw angle (in degrees).
+ * @param result Rotated vector.
+ */
+stock void RotateVectorPitchYaw(float vec[3], float pitch, float yaw, float result[3])
+{
+ if (pitch != 0.0)
+ {
+ RotateVectorAxis(vec, view_as<float>({0.0, 1.0, 0.0}), DegToRad(pitch), result);
+ }
+ if (yaw != 0.0)
+ {
+ RotateVectorAxis(result, view_as<float>({0.0, 0.0, 1.0}), DegToRad(yaw), result);
+ }
+}
+
+/**
+ * Attempts to return a valid spawn location.
+ *
+ * @param origin Spawn origin if found.
+ * @param angles Spawn angles if found.
+ * @return Whether a valid spawn point is found.
+ */
+stock bool GetValidSpawn(float origin[3], float angles[3])
+{
+ // Return true if the spawn found is truly valid (not in the ground or out of bounds)
+ bool foundValidSpawn;
+ bool searchCT;
+ float spawnOrigin[3];
+ float spawnAngles[3];
+ int spawnEntity = -1;
+ while (!foundValidSpawn)
+ {
+ if (searchCT)
+ {
+ spawnEntity = FindEntityByClassname(spawnEntity, "info_player_counterterrorist");
+ }
+ else
+ {
+ spawnEntity = FindEntityByClassname(spawnEntity, "info_player_terrorist");
+ }
+
+ if (spawnEntity != -1)
+ {
+ GetEntPropVector(spawnEntity, Prop_Data, "m_vecOrigin", spawnOrigin);
+ GetEntPropVector(spawnEntity, Prop_Data, "m_angRotation", spawnAngles);
+ if (IsSpawnValid(spawnOrigin))
+ {
+ origin = spawnOrigin;
+ angles = spawnAngles;
+ foundValidSpawn = true;
+ }
+ }
+ else if (!searchCT)
+ {
+ searchCT = true;
+ }
+ else
+ {
+ break;
+ }
+ }
+ return foundValidSpawn;
+}
+
+/**
+ * Check whether a position is a valid spawn location.
+ * A spawn location is considered valid if it is in bounds and not stuck inside the ground.
+ *
+ * @param origin Origin vector.
+ * @return Whether the origin is a valid spawn location.
+ */
+stock bool IsSpawnValid(float origin[3])
+{
+ Handle trace = TR_TraceHullFilterEx(origin, origin, PLAYER_MINS, PLAYER_MAXS, MASK_PLAYERSOLID, TraceEntityFilterPlayers);
+ if (!TR_StartSolid(trace) && !TR_AllSolid(trace) && TR_GetFraction(trace) == 1.0)
+ {
+ delete trace;
+ return true;
+ }
+ delete trace;
+ return false;
+}
+
+/**
+ * Get an entity's origin, angles, its bounding box's center and the distance from the center to its bounding box's edges.
+ *
+ * @param entity Index of the entity.
+ * @param origin Entity's origin.
+ * @param center Center of the entity's bounding box.
+ * @param angles Entity's angles.
+ * @param distFromCenter The distance between the center of the entity's bounding box and its edges.
+ */
+stock void GetEntityPositions(int entity, float origin[3], float center[3], float angles[3], float distFromCenter[3])
+{
+ int ent = entity;
+ float maxs[3], mins[3];
+ GetEntPropVector(ent, Prop_Send, "m_vecOrigin", origin);
+ // Take parent entities into account.
+ while (GetEntPropEnt(ent, Prop_Send, "moveparent") != -1)
+ {
+ ent = GetEntPropEnt(ent, Prop_Send, "moveparent");
+ float tempOrigin[3];
+ GetEntPropVector(ent, Prop_Send, "m_vecOrigin", tempOrigin);
+ for (int i = 0; i < 3; i++)
+ {
+ origin[i] += tempOrigin[i];
+ }
+ }
+
+ GetEntPropVector(ent, Prop_Data, "m_angRotation", angles);
+
+ GetEntPropVector(ent, Prop_Send, "m_vecMaxs", maxs);
+ GetEntPropVector(ent, Prop_Send, "m_vecMins", mins);
+ for (int i = 0; i < 3; i++)
+ {
+ center[i] = origin[i] + (maxs[i] + mins[i]) / 2;
+ distFromCenter[i] = (maxs[i] - mins[i]) / 2;
+ }
+}
+
+/**
+ * Find a valid position around a timer.
+ *
+ * @param entity Index of the timer entity.
+ * @param originDest Result origin if a valid position is found.
+ * @param anglesDest Result angles if a valid position is found.
+ * @return Whether a valid position is found.
+ */
+stock bool FindValidPositionAroundTimerEntity(int entity, float originDest[3], float anglesDest[3], bool isButton)
+{
+ float origin[3], center[3], angles[3], distFromCenter[3];
+ GetEntityPositions(entity, origin, center, angles, distFromCenter);
+ float extraOffset[3];
+ if (isButton) // Test several positions within button press range.
+ {
+ extraOffset[0] = 32.0;
+ extraOffset[1] = 32.0;
+ extraOffset[2] = 32.0;
+ }
+ else // Test positions at the inner surface of the zone.
+ {
+ extraOffset[0] = -(PLAYER_MAXS[0] - PLAYER_MINS[0]) - 1.03125;
+ extraOffset[1] = -(PLAYER_MAXS[1] - PLAYER_MINS[1]) - 1.03125;
+ extraOffset[2] = -(PLAYER_MAXS[2] - PLAYER_MINS[2]) - 1.03125;
+ }
+ if (FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest))
+ {
+ return true;
+ }
+ // Test the positions right next to the timer button/zones if the tests above fail.
+ // This can fail when the timer has a cover brush over it.
+ extraOffset[0] = 0.03125;
+ extraOffset[1] = 0.03125;
+ extraOffset[2] = 0.03125;
+ return FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest);
+}
+
+static bool FindValidPositionAroundCenter(float center[3], float distFromCenter[3], float extraOffset[3], float originDest[3], float anglesDest[3])
+{
+ float testOrigin[3];
+ int x, y;
+
+ for (int i = 0; i < 3; i++)
+ {
+ // The search starts from the center then outwards to opposite directions.
+ x = i == 2 ? -1 : i;
+ for (int j = 0; j < 3; j++)
+ {
+ y = j == 2 ? -1 : j;
+ for (int z = -1; z <= 1; z++)
+ {
+ testOrigin = center;
+ testOrigin[0] = testOrigin[0] + (distFromCenter[0] + extraOffset[0]) * x + (PLAYER_MAXS[0] - PLAYER_MINS[0]) * x * 0.5;
+ testOrigin[1] = testOrigin[1] + (distFromCenter[1] + extraOffset[1]) * y + (PLAYER_MAXS[1] - PLAYER_MINS[1]) * y * 0.5;
+ testOrigin[2] = testOrigin[2] + (distFromCenter[2] + extraOffset[2]) * z + (PLAYER_MAXS[2] - PLAYER_MINS[2]) * z;
+
+ // Check if there's a line of sight towards the zone as well.
+ if (IsSpawnValid(testOrigin) && CanSeeBox(testOrigin, center, distFromCenter))
+ {
+ originDest = testOrigin;
+ // Always look towards the center.
+ float offsetVector[3];
+ offsetVector[0] = -(distFromCenter[0] + extraOffset[0]) * x;
+ offsetVector[1] = -(distFromCenter[1] + extraOffset[1]) * y;
+ offsetVector[2] = -(distFromCenter[2] + extraOffset[2]) * z;
+ GetVectorAngles(offsetVector, anglesDest);
+ anglesDest[2] = 0.0; // Roll should always be 0.0
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+static bool CanSeeBox(float origin[3], float center[3], float distFromCenter[3])
+{
+ float traceOrigin[3], traceDest[3], mins[3], maxs[3];
+
+ CopyVector(origin, traceOrigin);
+
+
+ SubtractVectors(center, distFromCenter, mins);
+ AddVectors(center, distFromCenter, maxs);
+
+ for (int i = 0; i < 3; i++)
+ {
+ mins[i] += 0.03125;
+ maxs[i] -= 0.03125;
+ traceDest[i] = FloatClamp(traceOrigin[i], mins[i], maxs[i]);
+ }
+ int mask = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE;
+ Handle trace = TR_TraceRayFilterEx(traceOrigin, traceDest, mask, RayType_EndPoint, TraceEntityFilterPlayers);
+ if (TR_DidHit(trace))
+ {
+ float end[3];
+ TR_GetEndPosition(end, trace);
+ for (int i = 0; i < 3; i++)
+ {
+ if (end[i] != traceDest[i])
+ {
+ delete trace;
+ return false;
+ }
+ }
+ }
+ delete trace;
+ return true;
+}
+
+/**
+ * Gets entity index from the address to an entity.
+ *
+ * @param pEntity Entity address.
+ * @return Entity index.
+ * @error Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle.
+ */
+stock int GOKZGetEntityFromAddress(Address pEntity)
+{
+ static int offs_RefEHandle;
+ if (offs_RefEHandle)
+ {
+ return EntRefToEntIndex(LoadFromAddress(pEntity + view_as<Address>(offs_RefEHandle), NumberType_Int32) | (1 << 31));
+ }
+
+ // if we don't have it already, attempt to lookup offset based on SDK information
+ // CWorld is derived from CBaseEntity so it should have both offsets
+ int offs_angRotation = FindDataMapInfo(0, "m_angRotation"), offs_vecViewOffset = FindDataMapInfo(0, "m_vecViewOffset");
+ if (offs_angRotation == -1)
+ {
+ SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_angRotation");
+ }
+ else if (offs_vecViewOffset == -1)
+ {
+ SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_vecViewOffset");
+ }
+ else if ((offs_angRotation + 0x0C) != (offs_vecViewOffset - 0x04))
+ {
+ char game[32];
+ GetGameFolderName(game, sizeof(game));
+ SetFailState("Could not confirm offset of CBaseEntity::m_RefEHandle (incorrect assumption for game '%s'?)", game);
+ }
+
+ // offset seems right, cache it for the next call
+ offs_RefEHandle = offs_angRotation + 0x0C;
+ return GOKZGetEntityFromAddress(pEntity);
+}
+
+/**
+ * Gets client index from CGameMovement class.
+ *
+ * @param addr Address of CGameMovement class.
+ * @param offsetCGameMovement_player Offset of CGameMovement::player.
+ * @return Client index.
+ * @error Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle.
+ */
+stock int GOKZGetClientFromGameMovementAddress(Address addr, int offsetCGameMovement_player)
+{
+ Address playerAddr = view_as<Address>(LoadFromAddress(view_as<Address>(view_as<int>(addr) + offsetCGameMovement_player), NumberType_Int32));
+ return GOKZGetEntityFromAddress(playerAddr);
+}
+
+/**
+ * Gets the nearest point in the oriented bounding box of an entity to a point.
+ *
+ * @param entity Entity index.
+ * @param origin Point's origin.
+ * @param result Result point.
+ */
+stock void CalcNearestPoint(int entity, float origin[3], float result[3])
+{
+ float entOrigin[3], entMins[3], entMaxs[3], trueMins[3], trueMaxs[3];
+ GetEntPropVector(entity, Prop_Send, "m_vecOrigin", entOrigin);
+ GetEntPropVector(entity, Prop_Send, "m_vecMaxs", entMaxs);
+ GetEntPropVector(entity, Prop_Send, "m_vecMins", entMins);
+
+ AddVectors(entOrigin, entMins, trueMins);
+ AddVectors(entOrigin, entMaxs, trueMaxs);
+
+ for (int i = 0; i < 3; i++)
+ {
+ result[i] = FloatClamp(origin[i], trueMins[i], trueMaxs[i]);
+ }
+}
+
+/**
+ * Get the shortest distance from P to the (infinite) line through vLineA and vLineB.
+ *
+ * @param P Point's origin.
+ * @param vLineA Origin of the first point of the line.
+ * @param vLineB Origin of the first point of the line.
+ * @return The shortest distance from the point to the line.
+ */
+stock float CalcDistanceToLine(float P[3], float vLineA[3], float vLineB[3])
+{
+ float vClosest[3];
+ float vDir[3];
+ float t;
+ float delta[3];
+ SubtractVectors(vLineB, vLineA, vDir);
+ float div = GetVectorDotProduct(vDir, vDir);
+ if (div < EPSILON)
+ {
+ t = 0.0;
+ }
+ else
+ {
+ t = (GetVectorDotProduct(vDir, P) - GetVectorDotProduct(vDir, vLineA)) / div;
+ }
+ for (int i = 0; i < 3; i++)
+ {
+ vClosest[i] = vLineA[i] + vDir[i]*t;
+ }
+ SubtractVectors(P, vClosest, delta);
+ return GetVectorLength(delta);
+}
+
+/**
+ * Gets the ideal amount of time the text should be held for HUD messages.
+ *
+ * The message buffer is only 16 slots long, and it is shared between 6 channels maximum.
+ * Assuming a message is sent every game frame, each channel used should be only taking around 2.5 slots on average.
+ * This also assumes all channels are used equally (so no other plugin taking all the channel buffer for itself).
+ * We want to use as much of the message buffer as possible to take into account latency variances.
+ *
+ * @param interval HUD message update interval, in tick intervals.
+ * @return How long the text should be held for.
+ */
+stock float GetTextHoldTime(int interval)
+{
+ return 3 * interval * GetTickInterval();
+}
diff --git a/sourcemod/scripting/include/gokz/anticheat.inc b/sourcemod/scripting/include/gokz/anticheat.inc
new file mode 100644
index 0000000..7fa5409
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/anticheat.inc
@@ -0,0 +1,168 @@
+/*
+ gokz-anticheat Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_anticheat_included_
+#endinput
+#endif
+#define _gokz_anticheat_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum ACReason:
+{
+ ACReason_BhopMacro = 0,
+ ACReason_BhopHack,
+ ACREASON_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define AC_MAX_BUTTON_SAMPLES 40
+#define AC_MAX_BHOP_GROUND_TICKS 8
+#define AC_MAX_BHOP_SAMPLES 30
+#define AC_BINDEXCEPTION_SAMPLES 5
+#define AC_LOG_PATH "logs/gokz-anticheat.log"
+
+stock char gC_ACReasons[ACREASON_COUNT][] =
+{
+ "BHop Macro",
+ "BHop Hack"
+};
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when gokz-anticheat suspects a player of cheating.
+ *
+ * @param client Client index.
+ * @param reason Reason for suspicion.
+ * @param notes Additional reasoning, description etc.
+ * @param stats Data supporting the suspicion e.g. scroll pattern.
+ */
+forward void GOKZ_AC_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats);
+
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets the number of recent bhop samples available for a player.
+ *
+ * @param client Client index.
+ * @return Number of bhop samples available.
+ */
+native int GOKZ_AC_GetSampleSize(int client);
+
+/**
+ * Gets whether a player hit a perfect bhop for a number of
+ * recent bhops. Buffer must be large enough to fit the sample
+ * size.
+ *
+ * @param client Client index.
+ * @param buffer Buffer for perfect bhop booleans, with the first element being the most recent bhop.
+ * @param sampleSize Maximum recent bhop samples.
+ * @return Number of bhop samples.
+ */
+native int GOKZ_AC_GetHitPerf(int client, bool[] buffer, int sampleSize);
+
+/**
+ * Gets a player's number of perfect bhops out of a sample
+ * size of bhops.
+ *
+ * @param client Client index.
+ * @param sampleSize Maximum recent bhop samples to include in calculation.
+ * @return Player's number of perfect bhops.
+ */
+native int GOKZ_AC_GetPerfCount(int client, int sampleSize);
+
+/**
+ * Gets a player's ratio of perfect bhops to normal bhops.
+ *
+ * @param client Client index.
+ * @param sampleSize Maximum recent bhop samples to include in calculation.
+ * @return Player's ratio of perfect bhops to normal bhops.
+ */
+native float GOKZ_AC_GetPerfRatio(int client, int sampleSize);
+
+/**
+ * Gets a player's jump input counts for a number of recent
+ * bhops. Buffer must be large enough to fit the sample size.
+ *
+ * @param client Client index.
+ * @param buffer Buffer for jump input counts, with the first element being the most recent bhop.
+ * @param sampleSize Maximum recent bhop samples.
+ * @return Number of bhop samples.
+ */
+native int GOKZ_AC_GetJumpInputs(int client, int[] buffer, int sampleSize);
+
+/**
+ * Gets a player's average number of jump inputs for a number
+ * of recent bhops.
+ *
+ * @param client Client index.
+ * @param sampleSize Maximum recent bhop samples to include in calculation.
+ * @return Player's average number of jump inputs.
+ */
+native float GOKZ_AC_GetAverageJumpInputs(int client, int sampleSize);
+
+/**
+ * Gets a player's jump input counts prior to a number of recent
+ * bhops. Buffer must be large enough to fit the sample size.
+ * Includes the jump input that resulted in the jump.
+ *
+ * @param client Client index.
+ * @param buffer Buffer for jump input counts, with the first element being the most recent bhop.
+ * @param sampleSize Maximum recent bhop samples.
+ * @return Number of bhop samples.
+ */
+native int GOKZ_AC_GetPreJumpInputs(int client, int[] buffer, int sampleSize);
+
+/**
+ * Gets a player's jump input counts after a number of recent
+ * bhops. Buffer must be large enough to fit the sample size.
+ * Excludes the jump input that resulted in the jump.
+ *
+ * @param client Client index.
+ * @param buffer Buffer for jump input counts, with the first element being the most recent bhop.
+ * @param sampleSize Maximum recent bhop samples.
+ * @return Number of bhop samples.
+ */
+native int GOKZ_AC_GetPostJumpInputs(int client, int[] buffer, int sampleSize);
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_anticheat =
+{
+ name = "gokz-anticheat",
+ file = "gokz-anticheat.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_anticheat_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_AC_GetSampleSize");
+ MarkNativeAsOptional("GOKZ_AC_GetHitPerf");
+ MarkNativeAsOptional("GOKZ_AC_GetPerfCount");
+ MarkNativeAsOptional("GOKZ_AC_GetPerfRatio");
+ MarkNativeAsOptional("GOKZ_AC_GetJumpInputs");
+ MarkNativeAsOptional("GOKZ_AC_GetAverageJumpInputs");
+ MarkNativeAsOptional("GOKZ_AC_GetPreJumpInputs");
+ MarkNativeAsOptional("GOKZ_AC_GetPostJumpInputs");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/chat.inc b/sourcemod/scripting/include/gokz/chat.inc
new file mode 100644
index 0000000..0264a57
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/chat.inc
@@ -0,0 +1,45 @@
+/*
+ gokz-chat Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_chat_included_
+#endinput
+#endif
+#define _gokz_chat_included_
+
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets whether a mode is loaded.
+ *
+ * @param client Client.
+ * @param tag Tag to prepend to the player name in chat.
+ * @param color Color to use for the tag.
+ */
+native void GOKZ_CH_SetChatTag(int client, const char[] tag, const char[] color);
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_chat =
+{
+ name = "gokz-chat",
+ file = "gokz-chat.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_chat_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_CH_SetChatTag");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/core.inc b/sourcemod/scripting/include/gokz/core.inc
new file mode 100644
index 0000000..fb450d1
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/core.inc
@@ -0,0 +1,1920 @@
+/*
+ gokz-core Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_core_included_
+#endinput
+#endif
+#define _gokz_core_included_
+
+#include <cstrike>
+#include <regex>
+#include <topmenus>
+
+#include <gokz>
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ TimeType_Nub = 0,
+ TimeType_Pro,
+ TIMETYPE_COUNT
+};
+
+enum
+{
+ MapPrefix_Other = 0,
+ MapPrefix_KZPro,
+ MAPPREFIX_COUNT
+};
+
+enum StartPositionType:
+{
+ StartPositionType_Spawn,
+ StartPositionType_Custom,
+ StartPositionType_MapButton,
+ StartPositionType_MapStart,
+ STARTPOSITIONTYPE_COUNT
+};
+
+enum CourseTimerType:
+{
+ CourseTimerType_None,
+ CourseTimerType_Default,
+ CourseTimerType_Button,
+ CourseTimerType_ZoneLegacy,
+ CourseTimerType_ZoneNew,
+ CourseTimerType_COUNT
+};
+
+enum OptionProp:
+{
+ OptionProp_Cookie = 0,
+ OptionProp_Type,
+ OptionProp_DefaultValue,
+ OptionProp_MinValue,
+ OptionProp_MaxValue,
+ OPTIONPROP_COUNT
+};
+
+enum OptionType:
+{
+ OptionType_Int = 0,
+ OptionType_Float
+};
+
+enum Option:
+{
+ OPTION_INVALID = -1,
+ Option_Mode,
+ Option_Style,
+ Option_CheckpointMessages,
+ Option_CheckpointSounds,
+ Option_TeleportSounds,
+ Option_ErrorSounds,
+ Option_VirtualButtonIndicators,
+ Option_TimerButtonZoneType,
+ Option_ButtonThroughPlayers,
+ Option_Safeguard,
+ OPTION_COUNT
+};
+
+enum
+{
+ Mode_Vanilla = 0,
+ Mode_SimpleKZ,
+ Mode_KZTimer,
+ MODE_COUNT
+};
+
+enum
+{
+ Style_Normal = 0,
+ STYLE_COUNT
+};
+
+enum
+{
+ CheckpointMessages_Disabled = 0,
+ CheckpointMessages_Enabled,
+ CHECKPOINTMESSAGES_COUNT
+};
+
+enum
+{
+ CheckpointSounds_Disabled = 0,
+ CheckpointSounds_Enabled,
+ CHECKPOINTSOUNDS_COUNT
+};
+
+enum
+{
+ TeleportSounds_Disabled = 0,
+ TeleportSounds_Enabled,
+ TELEPORTSOUNDS_COUNT
+};
+
+enum
+{
+ ErrorSounds_Disabled = 0,
+ ErrorSounds_Enabled,
+ ERRORSOUNDS_COUNT
+};
+
+enum
+{
+ VirtualButtonIndicators_Disabled = 0,
+ VirtualButtonIndicators_Enabled,
+ VIRTUALBUTTONINDICATORS_COUNT
+};
+
+enum
+{
+ TimerButtonZoneType_BothButtons = 0,
+ TimerButtonZoneType_EndZone,
+ TimerButtonZoneType_BothZones,
+ TIMERBUTTONZONETYPE_COUNT
+};
+
+enum
+{
+ ButtonThroughPlayers_Disabled = 0,
+ ButtonThroughPlayers_Enabled,
+ BUTTONTHROUGHPLAYERS_COUNT
+};
+
+enum
+{
+ Safeguard_Disabled = 0,
+ Safeguard_EnabledNUB,
+ Safeguard_EnabledPRO,
+ SAFEGUARD_COUNT
+};
+
+enum
+{
+ ModeCVar_Accelerate = 0,
+ ModeCVar_AccelerateUseWeaponSpeed,
+ ModeCVar_AirAccelerate,
+ ModeCVar_AirMaxWishSpeed,
+ ModeCVar_EnableBunnyhopping,
+ ModeCVar_Friction,
+ ModeCVar_Gravity,
+ ModeCVar_JumpImpulse,
+ ModeCVar_LadderScaleSpeed,
+ ModeCVar_LedgeMantleHelper,
+ ModeCVar_MaxSpeed,
+ ModeCVar_MaxVelocity,
+ ModeCVar_StaminaJumpCost,
+ ModeCVar_StaminaLandCost,
+ ModeCVar_StaminaMax,
+ ModeCVar_StaminaRecoveryRate,
+ ModeCVar_StandableNormal,
+ ModeCVar_TimeBetweenDucks,
+ ModeCVar_WalkableNormal,
+ ModeCVar_WaterAccelerate,
+ MoveCVar_WaterMoveSpeedMultiplier,
+ MoveCVar_WaterSwimMode,
+ MoveCVar_WeaponEncumbrancePerItem,
+ ModeCVar_WeaponEncumbranceScale,
+ MODECVAR_COUNT
+};
+
+// NOTE: gokz-core/map/entlump.sp
+enum EntlumpTokenType
+{
+ EntlumpTokenType_OpenBrace, // {
+ EntlumpTokenType_CloseBrace, // }
+ EntlumpTokenType_Identifier, // everything that's inside quotations
+ EntlumpTokenType_Unknown,
+ EntlumpTokenType_EndOfStream
+};
+
+// NOTE: gokz-core/map/triggers.sp
+// NOTE: corresponds to climb_teleport_type in kz_mapping_api.fgd
+enum TeleportType
+{
+ TeleportType_Invalid = -1,
+ TeleportType_Normal,
+ TeleportType_MultiBhop,
+ TeleportType_SingleBhop,
+ TeleportType_SequentialBhop,
+ TELEPORTTYPE_COUNT
+};
+
+enum TriggerType
+{
+ TriggerType_Invalid = 0,
+ TriggerType_Teleport,
+ TriggerType_Antibhop
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define GOKZ_CHECKPOINT_VERSION 2
+#define GOKZ_MAX_CHECKPOINTS 2048
+#define GOKZ_MAX_COURSES 100
+
+#define GOKZ_BHOP_NO_CHECKPOINT_TIME 0.15
+#define GOKZ_MULT_NO_CHECKPOINT_TIME 0.11
+#define GOKZ_LADDER_NO_CHECKPOINT_TIME 1.5
+#define GOKZ_PAUSE_COOLDOWN 1.0
+#define GOKZ_TIMER_START_NO_TELEPORT_TICKS 4
+#define GOKZ_TIMER_START_GROUND_TICKS 4
+#define GOKZ_TIMER_START_NOCLIP_TICKS 4
+#define GOKZ_JUMPSTATS_NOCLIP_RESET_TICKS 4
+#define GOKZ_TIMER_SOUND_COOLDOWN 0.15
+#define GOKZ_VIRTUAL_BUTTON_USE_DETECTION_TIME 2.0
+#define GOKZ_TURNBIND_COOLDOWN 0.3
+
+#define GOKZ_MAPPING_API_VERSION_NONE 0 // the map doesn't have a mapping api version
+#define GOKZ_MAPPING_API_VERSION 1
+
+#define GOKZ_ANTI_BHOP_TRIGGER_DEFAULT_DELAY 0.2
+#define GOKZ_TELEPORT_TRIGGER_DEFAULT_TYPE TeleportType_Normal
+#define GOKZ_TELEPORT_TRIGGER_DEFAULT_DELAY 0.0
+#define GOKZ_TELEPORT_TRIGGER_DEFAULT_USE_DEST_ANGLES true
+#define GOKZ_TELEPORT_TRIGGER_DEFAULT_RESET_SPEED true
+#define GOKZ_TELEPORT_TRIGGER_DEFAULT_RELATIVE_DESTINATION false
+#define GOKZ_TELEPORT_TRIGGER_DEFAULT_REORIENT_PLAYER false
+#define GOKZ_TELEPORT_TRIGGER_BHOP_MIN_DELAY 0.08
+
+#define GOKZ_SOUND_CHECKPOINT "buttons/blip1.wav"
+#define GOKZ_SOUND_TELEPORT "buttons/blip1.wav"
+#define GOKZ_SOUND_TIMER_STOP "buttons/button18.wav"
+
+#define GOKZ_START_NAME "climb_start"
+#define GOKZ_BONUS_START_NAME_REGEX "^climb_bonus(\\d+)_start$"
+#define GOKZ_BONUS_END_NAME_REGEX "^climb_bonus(\\d+)_end$"
+
+#define GOKZ_START_BUTTON_NAME "climb_startbutton"
+#define GOKZ_END_BUTTON_NAME "climb_endbutton"
+#define GOKZ_BONUS_START_BUTTON_NAME_REGEX "^climb_bonus(\\d+)_startbutton$"
+#define GOKZ_BONUS_END_BUTTON_NAME_REGEX "^climb_bonus(\\d+)_endbutton$"
+#define GOKZ_ANTI_BHOP_TRIGGER_NAME "climb_anti_bhop"
+#define GOKZ_ANTI_CP_TRIGGER_NAME "climb_anti_checkpoint"
+#define GOKZ_ANTI_PAUSE_TRIGGER_NAME "climb_anti_pause"
+#define GOKZ_ANTI_JUMPSTAT_TRIGGER_NAME "climb_anti_jumpstat"
+#define GOKZ_BHOP_RESET_TRIGGER_NAME "climb_bhop_reset"
+#define GOKZ_TELEPORT_TRIGGER_NAME "climb_teleport"
+
+#define GOKZ_START_ZONE_NAME "climb_startzone"
+#define GOKZ_END_ZONE_NAME "climb_endzone"
+#define GOKZ_BONUS_START_ZONE_NAME_REGEX "^climb_bonus(\\d+)_startzone$"
+#define GOKZ_BONUS_END_ZONE_NAME_REGEX "^climb_bonus(\\d+)_endzone$"
+
+#define GOKZ_CFG_SERVER "sourcemod/gokz/gokz.cfg"
+#define GOKZ_CFG_OPTIONS "cfg/sourcemod/gokz/options.cfg"
+#define GOKZ_CFG_OPTIONS_SORTING "cfg/sourcemod/gokz/options_menu_sorting.cfg"
+#define GOKZ_CFG_OPTIONS_ROOT "Options"
+#define GOKZ_CFG_OPTIONS_DESCRIPTION "description"
+#define GOKZ_CFG_OPTIONS_DEFAULT "default"
+
+#define GOKZ_OPTION_MAX_NAME_LENGTH 30
+#define GOKZ_OPTION_MAX_DESC_LENGTH 255
+#define GENERAL_OPTION_CATEGORY "General"
+
+// TODO: where do i put the defines?
+#define GOKZ_BSP_HEADER_IDENTIFIER (('P' << 24) | ('S' << 16) | ('B' << 8) | 'V')
+#define GOKZ_ENTLUMP_MAX_KEY 32
+#define GOKZ_ENTLUMP_MAX_VALUE 1024
+
+#define GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH 256
+
+#define CHAR_ESCAPE view_as<char>(27)
+
+#define GOKZ_SAFEGUARD_RESTART_MIN_DELAY 0.6
+#define GOKZ_SAFEGUARD_RESTART_MAX_DELAY 5.0
+
+// Prevents the player from retouching a trigger too often.
+#define GOKZ_MAX_RETOUCH_TRIGGER_COUNT 4
+
+stock char gC_TimeTypeNames[TIMETYPE_COUNT][] =
+{
+ "NUB",
+ "PRO"
+};
+
+stock char gC_ModeNames[MODE_COUNT][] =
+{
+ "Vanilla",
+ "SimpleKZ",
+ "KZTimer"
+};
+
+stock char gC_ModeNamesShort[MODE_COUNT][] =
+{
+ "VNL",
+ "SKZ",
+ "KZT"
+};
+
+stock char gC_ModeKeys[MODE_COUNT][] =
+{
+ "vanilla",
+ "simplekz",
+ "kztimer"
+};
+
+stock float gF_ModeVirtualButtonRanges[MODE_COUNT] =
+{
+ 0.0,
+ 32.0,
+ 70.0
+};
+
+stock char gC_ModeStartSounds[MODE_COUNT][] =
+{
+ "common/wpn_select.wav",
+ "buttons/button9.wav",
+ "buttons/button3.wav"
+};
+
+stock char gC_ModeEndSounds[MODE_COUNT][] =
+{
+ "common/wpn_select.wav",
+ "buttons/bell1.wav",
+ "buttons/button3.wav"
+};
+
+stock char gC_ModeFalseEndSounds[MODE_COUNT][] =
+{
+ "common/wpn_select.wav",
+ "buttons/button11.wav",
+ "buttons/button2.wav"
+};
+
+stock char gC_StyleNames[STYLE_COUNT][] =
+{
+ "Normal"
+};
+
+stock char gC_StyleNamesShort[STYLE_COUNT][] =
+{
+ "NRM"
+};
+
+stock char gC_CoreOptionNames[OPTION_COUNT][] =
+{
+ "GOKZ - Mode",
+ "GOKZ - Style",
+ "GOKZ - Checkpoint Messages",
+ "GOKZ - Checkpoint Sounds",
+ "GOKZ - Teleport Sounds",
+ "GOKZ - Error Sounds",
+ "GOKZ - VB Indicators",
+ "GOKZ - Timer Button Zone Type",
+ "GOKZ - Button Through Players",
+ "GOKZ - Safeguard"
+};
+
+stock char gC_CoreOptionDescriptions[OPTION_COUNT][] =
+{
+ "Movement Mode - 0 = Vanilla, 1 = SimpleKZ, 2 = KZTimer",
+ "Movement Style - 0 = Normal",
+ "Checkpoint Messages - 0 = Disabled, 1 = Enabled",
+ "Checkpoint Sounds - 0 = Disabled, 1 = Enabled",
+ "Teleport Sounds - 0 = Disabled, 1 = Enabled",
+ "Error Sounds - 0 = Disabled, 1 = Enabled",
+ "Virtual Button Indicators - 0 = Disabled, 1 = Enabled",
+ "Timer Button Zone Type - 0 = Both buttons, 1 = Only end zone, 2 = Both zones",
+ "Button Through Players - 0 = Disabled, 1 = Enabled",
+ "Safeguard - 0 = Disabled, 1 = Enabled (NUB), 2 = Enabled (PRO)"
+};
+
+stock char gC_CoreOptionPhrases[OPTION_COUNT][] =
+{
+ "Options Menu - Mode",
+ "Options Menu - Style",
+ "Options Menu - Checkpoint Messages",
+ "Options Menu - Checkpoint Sounds",
+ "Options Menu - Teleport Sounds",
+ "Options Menu - Error Sounds",
+ "Options Menu - Virtual Button Indicators",
+ "Options Menu - Timer Button Zone Type",
+ "Options Menu - Button Through Players",
+ "Options Menu - Safeguard"
+};
+
+stock char gC_TimerButtonZoneTypePhrases[TIMERBUTTONZONETYPE_COUNT][] =
+{
+ "Timer Button Zone Type - Both Buttons",
+ "Timer Button Zone Type - Only End Zone",
+ "Timer Button Zone Type - Both Zones"
+};
+
+stock char gC_SafeGuardPhrases[SAFEGUARD_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Safeguard - Enabled NUB",
+ "Safeguard - Enabled PRO"
+}
+
+stock int gI_CoreOptionCounts[OPTION_COUNT] =
+{
+ MODE_COUNT,
+ STYLE_COUNT,
+ CHECKPOINTMESSAGES_COUNT,
+ CHECKPOINTSOUNDS_COUNT,
+ TELEPORTSOUNDS_COUNT,
+ ERRORSOUNDS_COUNT,
+ VIRTUALBUTTONINDICATORS_COUNT,
+ TIMERBUTTONZONETYPE_COUNT,
+ BUTTONTHROUGHPLAYERS_COUNT,
+ SAFEGUARD_COUNT
+};
+
+stock int gI_CoreOptionDefaults[OPTION_COUNT] =
+{
+ Mode_KZTimer,
+ Style_Normal,
+ CheckpointMessages_Disabled,
+ CheckpointSounds_Enabled,
+ TeleportSounds_Disabled,
+ ErrorSounds_Enabled,
+ VirtualButtonIndicators_Disabled,
+ TimerButtonZoneType_BothButtons,
+ ButtonThroughPlayers_Enabled,
+ Safeguard_Disabled
+};
+
+stock char gC_ModeCVars[MODECVAR_COUNT][] =
+{
+ "sv_accelerate",
+ "sv_accelerate_use_weapon_speed",
+ "sv_airaccelerate",
+ "sv_air_max_wishspeed",
+ "sv_enablebunnyhopping",
+ "sv_friction",
+ "sv_gravity",
+ "sv_jump_impulse",
+ "sv_ladder_scale_speed",
+ "sv_ledge_mantle_helper",
+ "sv_maxspeed",
+ "sv_maxvelocity",
+ "sv_staminajumpcost",
+ "sv_staminalandcost",
+ "sv_staminamax",
+ "sv_staminarecoveryrate",
+ "sv_standable_normal",
+ "sv_timebetweenducks",
+ "sv_walkable_normal",
+ "sv_wateraccelerate",
+ "sv_water_movespeed_multiplier",
+ "sv_water_swim_mode",
+ "sv_weapon_encumbrance_per_item",
+ "sv_weapon_encumbrance_scale"
+};
+
+
+// =====[ STRUCTS ]=====
+
+enum struct Checkpoint
+{
+ float origin[3];
+ float angles[3];
+ float ladderNormal[3];
+ bool onLadder;
+ int groundEnt;
+
+ void Create(int client)
+ {
+ Movement_GetOrigin(client, this.origin);
+ Movement_GetEyeAngles(client, this.angles);
+ GetEntPropVector(client, Prop_Send, "m_vecLadderNormal", this.ladderNormal);
+ this.onLadder = Movement_GetMovetype(client) == MOVETYPE_LADDER;
+ this.groundEnt = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity");
+ }
+}
+
+enum struct UndoTeleportData
+{
+ float tempOrigin[3];
+ float tempAngles[3];
+ float origin[3];
+ float angles[3];
+ // Undo TP properties
+ bool lastTeleportOnGround;
+ bool lastTeleportInBhopTrigger;
+ bool lastTeleportInAntiCpTrigger;
+
+ void Init(int client, bool lastTeleportInBhopTrigger, bool lastTeleportOnGround, bool lastTeleportInAntiCpTrigger)
+ {
+ Movement_GetOrigin(client, this.tempOrigin);
+ Movement_GetEyeAngles(client, this.tempAngles);
+ this.lastTeleportInBhopTrigger = lastTeleportInBhopTrigger;
+ this.lastTeleportOnGround = lastTeleportOnGround;
+ this.lastTeleportInAntiCpTrigger = lastTeleportInAntiCpTrigger;
+ }
+
+ void Update()
+ {
+ this.origin = this.tempOrigin;
+ this.angles = this.tempAngles;
+ }
+}
+
+
+// NOTE: gokz-core/map/entlump.sp
+enum struct EntlumpToken
+{
+ EntlumpTokenType type;
+ char string[GOKZ_ENTLUMP_MAX_VALUE];
+}
+
+// NOTE: gokz-core/map/triggers.sp
+enum struct AntiBhopTrigger
+{
+ int entRef;
+ int hammerID;
+ float time;
+}
+
+enum struct TeleportTrigger
+{
+ int hammerID;
+ TeleportType type;
+ float delay;
+ char tpDestination[256];
+ bool useDestAngles;
+ bool resetSpeed;
+ bool relativeDestination;
+ bool reorientPlayer;
+}
+
+enum struct TouchedTrigger
+{
+ TriggerType triggerType;
+ int entRef; // entref of one of the TeleportTriggers
+ int startTouchTick; // tick where the player touched the trigger
+ int groundTouchTick; // tick where the player touched the ground
+}
+
+// Legacy triggers that activate timer buttons.
+enum struct TimerButtonTrigger
+{
+ int hammerID;
+ int course;
+ bool isStartTimer;
+}
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a player's options values are loaded from clientprefs.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnOptionsLoaded(int client);
+
+/**
+ * Called when a player's option's value is changed.
+ * Only called if client is in game.
+ *
+ * @param client Client index.
+ * @param option Option name.
+ * @param newValue New value of the option.
+ */
+forward void GOKZ_OnOptionChanged(int client, const char[] option, any newValue);
+
+/**
+ * Called when a player starts their timer.
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTimerStart(int client, int course);
+
+/**
+ * Called when a player has started their timer.
+ *
+ * @param client Client index.
+ * @param course Course number.
+ */
+forward void GOKZ_OnTimerStart_Post(int client, int course);
+
+/**
+ * Called when a player ends their timer.
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @param time Player's end time.
+ * @param teleportsUsed Number of teleports used by player.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTimerEnd(int client, int course, float time, int teleportsUsed);
+
+/**
+ * Called when a player has ended their timer.
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @param time Player's end time.
+ * @param teleportsUsed Number of teleports used by player.
+ */
+forward void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed);
+
+/**
+ * Called when the end timer message is printed to chat.
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @param time Player's end time.
+ * @param teleportsUsed Number of teleports used by player.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTimerEndMessage(int client, int course, float time, int teleportsUsed);
+
+/**
+ * Called when a player's timer has been forcefully stopped.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnTimerStopped(int client);
+
+/**
+ * Called when a player pauses.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnPause(int client);
+
+/**
+ * Called when a player has paused.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnPause_Post(int client);
+
+/**
+ * Called when a player resumes.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnResume(int client);
+
+/**
+ * Called when a player has resumed.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnResume_Post(int client);
+
+/**
+ * Called when a player makes a checkpoint.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnMakeCheckpoint(int client);
+
+/**
+ * Called when a player has made a checkpoint.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnMakeCheckpoint_Post(int client);
+
+/**
+ * Called when a player teleports to their checkpoint.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTeleportToCheckpoint(int client);
+
+/**
+ * Called when a player has teleported to their checkpoint.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnTeleportToCheckpoint_Post(int client);
+
+/**
+ * Called when a player goes to a previous checkpoint.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnPrevCheckpoint(int client);
+
+/**
+ * Called when a player has gone to a previous checkpoint.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnPrevCheckpoint_Post(int client);
+
+/**
+ * Called when a player goes to a next checkpoint.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnNextCheckpoint(int client);
+
+/**
+ * Called when a player has gone to a next checkpoint.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnNextCheckpoint_Post(int client);
+
+/**
+ * Called when a player teleports to start.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTeleportToStart(int client, int course);
+
+/**
+ * Called when a player has teleported to start.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ */
+forward void GOKZ_OnTeleportToStart_Post(int client, int course);
+
+/**
+ * Called when a player teleports to end.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTeleportToEnd(int client, int course);
+
+/**
+ * Called when a player has teleported to end.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ */
+forward void GOKZ_OnTeleportToEnd_Post(int client, int course);
+
+/**
+ * Called when a player undoes a teleport.
+ *
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnUndoTeleport(int client);
+
+/**
+ * Called when a player has undone a teleport.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnUndoTeleport_Post(int client);
+
+/**
+ * Called when a player has performed a counted teleport (teleport count went up)
+ * i.e. a catch-all for teleport to checkpoint, teleport to start, undo teleport etc.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnCountedTeleport_Post(int client);
+
+/**
+ * Called when a player's start position is set.
+ *
+ * @param client Client index.
+ * @param type Start position type.
+ * @param origin Start position origin.
+ * @param angles Start position eye angles.
+ */
+forward void GOKZ_OnStartPositionSet_Post(int client, StartPositionType type, const float origin[3], const float angles[3]);
+
+/**
+ * Called when player's begins a jump that is deemed valid.
+ * A jump is deemed invalid if a player is teleported.
+ *
+ * @param client Client index.
+ * @param jumped Whether player jumped.
+ * @param ladderJump Whether it was a ladder jump.
+ * @param jumpbug Whether player performed a jumpbug.
+ */
+forward void GOKZ_OnJumpValidated(int client, bool jumped, bool ladderJump, bool jumpbug);
+
+/**
+ * Called when player's current jump is invalidated.
+ * A jump is deemed invalid if a player is teleported.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnJumpInvalidated(int client);
+
+/**
+ * Called when a player has been switched to a team.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnJoinTeam(int client, int team);
+
+/**
+ * Called the first time a player spawns in on a team.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnFirstSpawn(int client);
+
+/**
+ * Called when a mode has been loaded.
+ *
+ * @param mode Mode loaded.
+ */
+forward void GOKZ_OnModeLoaded(int mode);
+
+/**
+ * Called when a mode has been unloaded.
+ *
+ * @param mode Mode unloaded.
+ */
+forward void GOKZ_OnModeUnloaded(int mode);
+
+/**
+ * Called when a plugin other than gokz-core calls a native
+ * that may affect a player's timer or teleport count in
+ * their favour e.g. GOKZ_StartTimer, GOKZ_EndTimer,
+ * GOKZ_SetTime and GOKZ_SetTeleportCount.
+ *
+ * @param plugin Handle of the calling plugin.
+ * @param client Client index.
+ * @return Plugin_Handled or Plugin_Stop to block, Plugin_Continue to proceed.
+ */
+forward Action GOKZ_OnTimerNativeCalledExternally(Handle plugin, int client);
+
+/**
+ * Called when the options menu has been created and 3rd
+ * party plugins can grab the handle or add categories.
+ *
+ * @param topMenu Options top menu handle.
+ */
+forward void GOKZ_OnOptionsMenuCreated(TopMenu topMenu);
+
+/**
+ * Called when the options menu is ready to have items added.
+ *
+ * @param topMenu Options top menu handle.
+ */
+forward void GOKZ_OnOptionsMenuReady(TopMenu topMenu);
+
+/**
+ * Called when a course is registered. A course is registered if both the
+ * start and end of it (e.g. timer buttons) have been detected.
+ *
+ * @param course Course number.
+ */
+forward void GOKZ_OnCourseRegistered(int course);
+
+/**
+ * Called when a player's run becomes invalidated.
+ * An invalidated run doesn't necessarily stop the timer.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_OnRunInvalidated(int client);
+
+/**
+ * Called when a sound is emitted to the client via GOKZ Core.
+ *
+ * @param client Client index.
+ * @param sample Sound file name relative to the "sound" folder.
+ * @param volume Sound volume.
+ * @param description Optional description.
+ * @return Plugin_Continue to allow the sound to be played, Plugin_Stop to block it,
+ * Plugin_Changed when any parameter has been modified.
+ */
+forward Action GOKZ_OnEmitSoundToClient(int client, const char[] sample, float &volume, const char[] description);
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets whether a mode is loaded.
+ *
+ * @param mode Mode.
+ * @return Whether mode is loaded.
+ */
+native bool GOKZ_GetModeLoaded(int mode);
+
+/**
+ * Gets the version number of a loaded mode.
+ *
+ * @param mode Mode.
+ * @return Version number of the mode, or -1 if not loaded.
+ */
+native int GOKZ_GetModeVersion(int mode);
+
+/**
+ * Sets whether a mode is loaded. To be used by mode plugins.
+ *
+ * @param mode Mode.
+ * @param loaded Whether mode is loaded.
+ * @param version Version number of the mode.
+ */
+native void GOKZ_SetModeLoaded(int mode, bool loaded, int version = -1);
+
+/**
+ * Gets the total number of loaded modes.
+ *
+ * @return Number of loaded modes.
+ */
+native int GOKZ_GetLoadedModeCount();
+
+/**
+ * Sets the player's current mode.
+ * If the player's timer is running, it will be stopped.
+ *
+ * @param client Client index.
+ * @param mode Mode.
+ * @return Whether the operation was successful.
+ */
+native bool GOKZ_SetMode(int client, int mode);
+
+/**
+ * Gets the Handle to the options top menu.
+ *
+ * @return Handle to the options top menu,
+ * or null if not created yet.
+ */
+native TopMenu GOKZ_GetOptionsTopMenu();
+
+/**
+ * Gets whether a course is registered. A course is registered if both the
+ * start and end of it (e.g. timer buttons) have been detected.
+ *
+ * @param course Course number.
+ * @return Whether course has been registered.
+ */
+native bool GOKZ_GetCourseRegistered(int course);
+
+/**
+ * Prints a message to a client's chat, formatting colours and optionally
+ * adding the chat prefix. If using the chat prefix, specify a colour at
+ * the beginning of the message e.g. "{default}Hello!".
+ *
+ * @param client Client index.
+ * @param addPrefix Whether to add the chat prefix.
+ * @param format Formatting rules.
+ * @param any Variable number of format parameters.
+ */
+native void GOKZ_PrintToChat(int client, bool addPrefix, const char[] format, any...);
+
+/**
+ * Prints a message to a client's chat, formatting colours and optionally
+ * adding the chat prefix. If using the chat prefix, specify a colour at
+ * the beginning of the message e.g. "{default}Hello!". Also prints the
+ * message to the server log.
+ *
+ * @param client Client index.
+ * @param addPrefix Whether to add the chat prefix.
+ * @param format Formatting rules.
+ * @param any Variable number of format parameters.
+ */
+native void GOKZ_PrintToChatAndLog(int client, bool addPrefix, const char[] format, any...);
+
+/**
+ * Starts a player's timer for a course on the current map.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @param allowMidair Whether player is allowed to start timer midair.
+ * @return Whether player's timer was started.
+ */
+native bool GOKZ_StartTimer(int client, int course, bool allowOffGround = false);
+
+/**
+ * Ends a player's timer for a course on the current map.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @return Whether player's timer was ended.
+ */
+native bool GOKZ_EndTimer(int client, int course);
+
+/**
+ * Forces a player's timer to stop. Intended for run invalidation.
+ *
+ * @param client Client index.
+ * @param playSound Whether to play the timer stop sound.
+ * @return Whether player's timer was stopped.
+ */
+native bool GOKZ_StopTimer(int client, bool playSound = true);
+
+/**
+ * Forces all players' timers to stop. Intended for run invalidation.
+ *
+ * @param playSound Whether to play the timer stop sound.
+ */
+native void GOKZ_StopTimerAll(bool playSound = true);
+
+/**
+ * Gets whether or not a player's timer is running i.e. isn't 'stopped'.
+ *
+ * @param client Client index.
+ * @return Whether player's timer is running.
+ */
+native bool GOKZ_GetTimerRunning(int client);
+
+/**
+ * Gets whether or not a player's timer is valid i.e the run is a valid run.
+ *
+ * @param client Client index.
+ * @return Whether player's timer is running.
+ */
+native bool GOKZ_GetValidTimer(int client);
+
+/**
+ * Gets the course a player is currently running.
+ *
+ * @param client Client index.
+ * @return Course number.
+ */
+native int GOKZ_GetCourse(int client);
+
+/**
+ * Set the player's current course.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param course Course number.
+ * @return Whether native was allowed to proceed.
+ */
+native bool GOKZ_SetCourse(int client, int course);
+
+/**
+ * Gets whether a player is paused.
+ *
+ * @param client Client index.
+ * @return Whether player is paused.
+ */
+native bool GOKZ_GetPaused(int client);
+
+/**
+ * Gets a player's current run time.
+ *
+ * @param client Client index.
+ * @return Player's current run time.
+ */
+native float GOKZ_GetTime(int client);
+
+/**
+ * Gets a player's current run time.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param time Run time to set to.
+ * @return Whether native was allowed to proceed.
+ */
+native bool GOKZ_SetTime(int client, float time);
+
+/**
+ * Mark a player's run as invalid without stopping the timer.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_InvalidateRun(int client);
+
+/**
+ * Gets a player's current checkpoint count.
+ *
+ * @param client Client index.
+ * @return Player's current checkpoint count.
+ */
+native int GOKZ_GetCheckpointCount(int client);
+
+/**
+ * Sets a player's current checkpoint count.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param cpCount Checkpoint count to set to.
+ * @return Whether native was allowed to proceed.
+ */
+native int GOKZ_SetCheckpointCount(int client, int cpCount);
+
+/**
+ * Gets checkpoint data of a player.
+ *
+ * @param client Client index.
+ * @return Client's checkpoint data.
+ */
+native ArrayList GOKZ_GetCheckpointData(int client);
+
+/**
+ * Sets checkpoint data of a player. The checkpoint data is assumed to be ordered.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param checkpoints Checkpoint data.
+ * @param version Checkpoint version.
+ * @return Whether native was allowed to proceed and operation was successful.
+ */
+native bool GOKZ_SetCheckpointData(int client, ArrayList checkpoints, int version);
+
+/**
+ * Get undo teleport data of a player.
+ *
+ * @param client Client index.
+ * @return ArrayList of length 1 containing player's undo teleport data.
+ */
+native ArrayList GOKZ_GetUndoTeleportData(int client);
+
+/**
+ * Set undo teleport data of a player.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param undoTeleportDataArray ArrayList of length 1 containing player's undo teleport data.
+ * @param version Checkpoint version.
+ * @return Whether native was allowed to proceed and operation was successful.
+ */
+native bool GOKZ_SetUndoTeleportData(int client, ArrayList undoTeleportDataArray, int version);
+
+/**
+ * Gets a player's current teleport count.
+ *
+ * @param client Client index.
+ * @return Player's current teleport count.
+ */
+native int GOKZ_GetTeleportCount(int client);
+
+/**
+ * Sets a player's current teleport count.
+ * This can be blocked by OnTimerNativeCalledExternally().
+ *
+ * @param client Client index.
+ * @param tpCount Teleport count to set to.
+ * @return Whether native was allowed to proceed.
+ */
+native bool GOKZ_SetTeleportCount(int client, int tpCount);
+
+/**
+ * Teleports a player to start, or respawns them.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_TeleportToStart(int client);
+
+/**
+ * Teleports a player to the start zone/button of the specified course.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ */
+native void GOKZ_TeleportToSearchStart(int client, int course);
+
+/**
+ * Gets the virtual button position the player currently has.
+ *
+ * @param client Client index.
+ * @param position Returns the client's virtual button position.
+ * @param isStart True to get the start button position, false for the end button.
+ * @return The course the button belongs to.
+ */
+native int GOKZ_GetVirtualButtonPosition(int client, float position[3], bool isStart);
+
+/**
+ * Sets the virtual button position the player currently has.
+ *
+ * @param client Client index.
+ * @param position The client's virtual button position.
+ * @param course The course the virtual button belongs to.
+ * @param isStart True to get the start button position, false for the end button.
+ */
+native void GOKZ_SetVirtualButtonPosition(int client, const float position[3], int course, bool isStart);
+
+/**
+ * Resets the player's virtual button.
+ *
+ * @param client Client index.
+ * @param isStart True to get the start button position, false for the end button.
+ */
+native void GOKZ_ResetVirtualButtonPosition(int client, bool isStart);
+
+/**
+ * Locks the virtual button position of a player.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_LockVirtualButtons(int client);
+
+/**
+ * Gets the start position the player currently has.
+ *
+ * @param client Client index.
+ * @param position Returns the client's start position.
+ * @param angles Returns the client's start angles.
+ * @return Player's current start position type.
+ */
+native StartPositionType GOKZ_GetStartPosition(int client, float position[3], float angles[3]);
+
+/**
+ * Sets the start position the player currently has.
+ *
+ * @param client Client index.
+ * @param type The start position type.
+ * @param position The client's start position.
+ * @param angles The client's start angles.
+ */
+native void GOKZ_SetStartPosition(int client, StartPositionType type, const float position[3], const float angles[3]);
+
+/**
+ * Gets the type of start position the player currently has.
+ * The "Spawn" type means teleport to start will respawn the player.
+ *
+ * @param client Client index.
+ * @return Player's current start position type.
+ */
+native StartPositionType GOKZ_GetStartPositionType(int client);
+
+/**
+ * Set the start position of the player to the start of a course.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ *
+ * @return False if the course start was not found.
+ */
+native bool GOKZ_SetStartPositionToMapStart(int client, int course);
+
+/**
+ * Teleports a player to end.
+ *
+ * @param client Client index.
+ * @param course Course index.
+ */
+native void GOKZ_TeleportToEnd(int client, int course);
+
+/**
+ * Set a new checkpoint at a player's current position.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_MakeCheckpoint(int client);
+
+/**
+ * Gets whether a player can make a new checkpoint.
+ * @param client Client index.
+ * @return Whether player can set a checkpoint.
+ */
+native bool GOKZ_GetCanMakeCheckpoint(int client);
+
+/**
+ * Teleports a player to their last checkpoint.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_TeleportToCheckpoint(int client);
+
+/**
+ * Gets whether a player can teleport to their checkpoint
+ * e.g. will return false if player has no checkpoints.
+ *
+ * @param client Client index.
+ * @return Whether player can teleport to checkpoint.
+ */
+native bool GOKZ_GetCanTeleportToCheckpoint(int client);
+
+/**
+ * Teleport a player back to a previous checkpoint.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_PrevCheckpoint(int client);
+
+/**
+ * Gets whether a player can go to their previous checkpoint
+ * e.g. will return false if player has no checkpoints.
+ *
+ * @param client Client index.
+ * @return Whether player can go to previous checkpoint.
+ */
+native bool GOKZ_GetCanPrevCheckpoint(int client);
+
+/**
+ * Teleport a player to a more recent checkpoint.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_NextCheckpoint(int client);
+
+/**
+ * Gets whether a player can go to their next checkpoint
+ * e.g. will return false if player has no checkpoints.
+ *
+ * @param client Client index.
+ * @return Whether player can go to next checkpoint.
+ */
+native bool GOKZ_GetCanNextCheckpoint(int client);
+
+/**
+ * Teleport a player to where they last teleported from.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_UndoTeleport(int client);
+
+/**
+ * Gets whether a player can undo their teleport
+ * e.g. will return false if teleport was from midair.
+ *
+ * @param client Client index.
+ * @return Whether player can undo teleport.
+ */
+native bool GOKZ_GetCanUndoTeleport(int client);
+
+/**
+ * Pause a player's timer and freeze them.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_Pause(int client);
+
+/**
+ * Gets whether a player can pause. Pausing is not allowed
+ * under some circumstance when the timer is running.
+ *
+ * @param client Client index.
+ */
+native bool GOKZ_GetCanPause(int client);
+
+/**
+ * Resumes a player's timer and unfreezes them.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_Resume(int client);
+
+/**
+ * Gets whether a player can resume. Resuming is not allowed
+ * under some circumstance when the timer is running.
+ *
+ * @param client Client index.
+ */
+native bool GOKZ_GetCanResume(int client);
+
+/**
+ * Toggles the paused state of a player.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_TogglePause(int client);
+
+/**
+ * Gets whether a player can teleport to start.
+ *
+ * @param client Client index.
+ * @return Whether player can teleport to start.
+ */
+native bool GOKZ_GetCanTeleportToStartOrEnd(int client);
+
+/**
+ * Plays the error sound to a player if they have the option enabled.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_PlayErrorSound(int client);
+
+/**
+ * Set the origin of a player without invalidating any jumpstats.
+ *
+ * Only use this in plugins that create a new mode!
+ *
+ * @param client Client index.
+ * @param origin The new origin.
+ */
+native void GOKZ_SetValidJumpOrigin(int client, const float origin[3]);
+
+/**
+ * Registers an option with gokz-core, which uses clientprefs to
+ * keep track of the option's value and to save it to a database.
+ * This also effectively provides natives and forwards for other
+ * plugins to access any options that have been registered.
+ *
+ * @param name Option name.
+ * @param description Option description.
+ * @param type Type to treat the option value as.
+ * @param defaultValue Default value of option.
+ * @param minValue Minimum value of option.
+ * @param maxValue Maximum value of option.
+ * @return Whether registration was successful.
+ */
+native bool GOKZ_RegisterOption(const char[] name, const char[] description, OptionType type, any defaultValue, any minValue, any maxValue);
+
+/**
+ * Gets a property of a registered option. If used outside of
+ * gokz-core to get the cookie, a clone of its Handle is returned.
+ *
+ * @param option Option name.
+ * @param prop Option property to get.
+ * @return Value of property, or -1 (int) if option isn't registered.
+ */
+native any GOKZ_GetOptionProp(const char[] option, OptionProp prop);
+
+/**
+ * Sets a property of a registered option. For safety and simplicity,
+ * the cookie property is read-only and will fail to be set.
+ *
+ * @param option Option name.
+ * @param prop Option property to set.
+ * @param value Value to set the property to.
+ * @return Whether option property was successfully set.
+ */
+native bool GOKZ_SetOptionProp(const char[] option, OptionProp prop, any value);
+
+/**
+ * Gets the current value of a player's option.
+ *
+ * @param client Client index.
+ * @param option Option name.
+ * @return Current value of option, or -1 (int) if option isn't registered.
+ */
+native any GOKZ_GetOption(int client, const char[] option);
+
+/**
+ * Sets a player's option's value. Fails if option doesn't exist,
+ * or if desired value is outside the registered value range.
+ *
+ * @param client Client index.
+ * @param option Option name.
+ * @param value New option value.
+ * @return Whether option was successfully set.
+ */
+native bool GOKZ_SetOption(int client, const char[] option, any value);
+
+/**
+ * Gets whether player's last takeoff was a perfect bunnyhop as adjusted by GOKZ.
+ *
+ * @param client Client index.
+ * @return Whether player's last takeoff was a GOKZ perfect b-hop.
+ */
+native bool GOKZ_GetHitPerf(int client);
+
+/**
+ * Sets whether player's last takeoff was a perfect bunnyhop as adjusted by GOKZ.
+ * Intended to be called by GOKZ mode plugins only.
+ *
+ * @param client Client index.
+ * @param hitPerf Whether player's last takeoff was a GOKZ perfect b-hop.
+ */
+native void GOKZ_SetHitPerf(int client, bool hitPerf);
+
+/**
+ * Gets a player's horizontal speed at the time of their last takeoff as recorded by GOKZ.
+ *
+ * @param client Client index.
+ * @return Player's last takeoff speed as recorded by GOKZ.
+ */
+native float GOKZ_GetTakeoffSpeed(int client);
+
+/**
+ * Sets a player's recorded horizontal speed at the time of their last takeoff.
+ * Intended to be called by GOKZ mode plugins only.
+ *
+ * @param client Client index.
+ * @param takeoffSpeed Player's last takeoff speed as recorded by GOKZ.
+ */
+native void GOKZ_SetTakeoffSpeed(int client, float takeoffSpeed);
+
+/**
+ * Gets whether a player's current or last jump/airtime is valid.
+ * A jump is deemed invalid if the player is teleported.
+ *
+ * @param client Client index.
+ * @return Validity of player's current or last jump.
+ */
+native bool GOKZ_GetValidJump(int client);
+
+/**
+ * Has a player switch to a team via GOKZ Core.
+ *
+ * @param client Client index.
+ * @param team Which team to switch to.
+ * @param restorePos Whether to restore saved position if leaving spectators.
+ * @param forceBroadcast Force JoinTeam forward calling even if client's team did not change.
+ */
+native void GOKZ_JoinTeam(int client, int team, bool restorePos = true, bool forceBroadcast = false);
+
+/**
+ * Emit a sound to a player via GOKZ Core.
+ * Sounds emitted by this native will call GOKZ_OnEmitSoundToClient forward.
+ *
+ * @param client Client index.
+ * @param sample Sound file name relative to the "sound" folder.
+ * @param volume Sound volume.
+ * @param description Optional description.
+ */
+native void GOKZ_EmitSoundToClient(int client, const char[] sample, float volume = SNDVOL_NORMAL, const char[] description = "");
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Makes a player join a team if they aren't on one and respawns them.
+ *
+ * @param client Client index.
+ * @param team Which team to switch to if not on one.
+ * @param restorePos Whether to restore saved position if leaving spectators.
+ */
+stock void GOKZ_RespawnPlayer(int client, int team = CS_TEAM_T, bool restorePos = true)
+{
+ if (IsSpectating(client))
+ {
+ GOKZ_JoinTeam(client, team, restorePos);
+ }
+ else
+ {
+ CS_RespawnPlayer(client);
+ }
+}
+
+/**
+ * Prints a message to all client's chat, formatting colours and optionally
+ * adding the chat prefix. If using the chat prefix, specify a colour at
+ * the beginning of the message e.g. "{default}Hello!".
+ *
+ * @param addPrefix Whether to add the chat prefix.
+ * @param format Formatting rules.
+ * @param any Variable number of format parameters.
+ */
+stock void GOKZ_PrintToChatAll(bool addPrefix, const char[] format, any...)
+{
+ char buffer[1024];
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), format, 3);
+ GOKZ_PrintToChat(client, addPrefix, buffer);
+ }
+ }
+}
+
+/**
+ * Prints a chat message to those spectating the client, formatting colours
+ * and optionally adding the chat prefix. If using the chat prefix, specify
+ * a colour at the beginning of the message e.g. "{default}Hello!".
+ *
+ * @param client Client index.
+ * @param addPrefix Whether to add the chat prefix.
+ * @param format Formatting rules.
+ * @param any Variable number of format parameters.
+ */
+stock void GOKZ_PrintToChatSpectators(int client, bool addPrefix, const char[] format, any...)
+{
+ char buffer[1024];
+ for (int target = 1; target <= MaxClients; target++)
+ {
+ if (IsClientInGame(target) && GetObserverTarget(target) == client)
+ {
+ SetGlobalTransTarget(target);
+ VFormat(buffer, sizeof(buffer), format, 4);
+ GOKZ_PrintToChat(target, addPrefix, buffer);
+ }
+ }
+}
+
+/**
+ * Gets the player's current time type.
+ *
+ * @param client Client index.
+ * @return Player's current time type.
+ */
+stock int GOKZ_GetTimeType(int client)
+{
+ return GOKZ_GetTimeTypeEx(GOKZ_GetTeleportCount(client));
+}
+
+/**
+ * Gets the time type given a teleport count.
+ *
+ * @param teleports Teleport count.
+ * @return Time type.
+ */
+stock int GOKZ_GetTimeTypeEx(int teleportCount)
+{
+ if (teleportCount == 0)
+ {
+ return TimeType_Pro;
+ }
+ return TimeType_Nub;
+}
+
+/**
+ * Clears and populates a menu with an item for each mode
+ * in order of the mode enumeration. Highlights the client's
+ * selected mode with an asterisk.
+ *
+ * @param client Client index to check selected mode.
+ * @param menu Menu to populate items with.
+ * @param disableUnloadedModes Draw items for unloaded modes as disabled.
+ */
+stock void GOKZ_MenuAddModeItems(int client, Menu menu, bool disableUnloadedModes)
+{
+ int selectedMode = GOKZ_GetCoreOption(client, Option_Mode);
+ char display[32];
+
+ menu.RemoveAllItems();
+
+ for (int mode = 0; mode < MODE_COUNT; mode++)
+ {
+ FormatEx(display, sizeof(display), "%s", gC_ModeNames[mode]);
+ // Add asterisk to selected mode
+ if (mode == selectedMode)
+ {
+ Format(display, sizeof(display), "%s*", display);
+ }
+
+ if (GOKZ_GetModeLoaded(mode))
+ {
+ menu.AddItem("", display, ITEMDRAW_DEFAULT);
+ }
+ else
+ {
+ menu.AddItem("", display, ITEMDRAW_DISABLED);
+ }
+ }
+}
+
+/**
+ * Increment an (integer-type) option's value.
+ * Loops back to min. value if max. value is exceeded.
+ *
+ * @param client Client index.
+ * @param option Option name.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_CycleOption(int client, const char[] option)
+{
+ int maxValue = GOKZ_GetOptionProp(option, OptionProp_MaxValue);
+ if (maxValue == -1)
+ {
+ return false;
+ }
+
+ int newValue = GOKZ_GetOption(client, option) + 1;
+ if (newValue > GOKZ_GetOptionProp(option, OptionProp_MaxValue))
+ {
+ newValue = GOKZ_GetOptionProp(option, OptionProp_MinValue);
+ }
+ return GOKZ_SetOption(client, option, newValue);
+}
+
+/**
+ * Returns whether an option is a gokz-core option.
+ *
+ * @param option Option name.
+ * @param optionEnum Variable to store enumerated gokz-core option (if it is one).
+ * @return Whether option is a gokz-core option.
+ */
+stock bool GOKZ_IsCoreOption(const char[] option, Option &optionEnum = OPTION_INVALID)
+{
+ for (Option i; i < OPTION_COUNT; i++)
+ {
+ if (StrEqual(option, gC_CoreOptionNames[i]))
+ {
+ optionEnum = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Gets a property of a gokz-core option.
+ *
+ * @param coreOption gokz-core option.
+ * @param prop Option property to get.
+ * @return Value of property, or -1 if option isn't registered.
+ */
+stock any GOKZ_GetCoreOptionProp(Option option, OptionProp prop)
+{
+ return GOKZ_GetOptionProp(gC_CoreOptionNames[option], prop);
+}
+
+/**
+ * Gets the current value of a player's gokz-core option.
+ *
+ * @param client Client index.
+ * @param option gokz-core option.
+ * @return Current value of option.
+ */
+stock any GOKZ_GetCoreOption(int client, Option option)
+{
+ return GOKZ_GetOption(client, gC_CoreOptionNames[option]);
+}
+
+/**
+ * Sets the player's gokz-core option's value.
+ *
+ * @param client Client index.
+ * @param option gokz-core option.
+ * @param value New option value.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_SetCoreOption(int client, Option option, any value)
+{
+ return GOKZ_SetOption(client, gC_CoreOptionNames[option], value);
+}
+
+/**
+ * Increment an integer-type gokz-core option's value.
+ * Loops back to '0' if max value is exceeded.
+ *
+ * @param client Client index.
+ * @param option gokz-core option.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_CycleCoreOption(int client, Option option)
+{
+ return GOKZ_CycleOption(client, gC_CoreOptionNames[option]);
+}
+
+/**
+ * Gets the current default mode.
+ *
+ * @return Default mode.
+ */
+stock int GOKZ_GetDefaultMode()
+{
+ return GOKZ_GetCoreOptionProp(Option_Mode, OptionProp_DefaultValue);
+}
+
+/**
+ * Returns whether a course number is a valid (within valid range).
+ *
+ * @param course Course number.
+ * @param bonus Whether to only consider bonus course numbers as valid.
+ * @return Whether course number is valid.
+ */
+stock bool GOKZ_IsValidCourse(int course, bool bonus = false)
+{
+ return (!bonus && course == 0) || (0 < course && course < GOKZ_MAX_COURSES);
+}
+
+/**
+ * Returns an integer from an entity's name as matched using a regular expression.
+ *
+ * @param entity Entity index.
+ * @param re Regular expression to match the integer with.
+ * @param substringID ID of the substring that will contain the integer.
+ * @returns Integer found in the entity's name, or -1 if not found.
+ */
+stock int GOKZ_MatchIntFromEntityName(int entity, Regex re, int substringID)
+{
+ int num = -1;
+ char buffer[32];
+ GetEntityName(entity, buffer, sizeof(buffer));
+
+ if (re.Match(buffer) > 0)
+ {
+ re.GetSubString(1, buffer, sizeof(buffer));
+ num = StringToInt(buffer);
+ }
+
+ return num;
+}
+
+/**
+ * Emits a sound to other players that are spectating the client.
+ * Sounds emitted by this function will call GOKZ_OnEmitSoundToClient forward.
+ *
+ * @param sample Sound file name relative to the "sound" folder.
+ * @param volume Sound volume.
+ * @param description Optional description.
+ */
+stock void GOKZ_EmitSoundToAll(const char[] sample, float volume = SNDVOL_NORMAL, const char[] description = "")
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsClientInGame(client))
+ {
+ GOKZ_EmitSoundToClient(client, sample, volume, description);
+ }
+ }
+}
+
+/**
+ * Emits a sound to other players that are spectating the client.
+ * Sounds emitted by this function will call GOKZ_OnEmitSoundToClient forward.
+ *
+ * @param client Client being spectated.
+ * @param sample Sound file name relative to the "sound" folder.
+ * @param volume Sound volume.
+ * @param description Optional description.
+ */
+stock void GOKZ_EmitSoundToClientSpectators(int client, const char[] sample, float volume = SNDVOL_NORMAL, const char[] description = "")
+{
+ for (int i = 1; i <= MaxClients; i++)
+ {
+ if (IsValidClient(i) && GetObserverTarget(i) == client)
+ {
+ GOKZ_EmitSoundToClient(i, sample, volume);
+ }
+ }
+}
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_core =
+{
+ name = "gokz-core",
+ file = "gokz-core.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_core_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_GetModeLoaded");
+ MarkNativeAsOptional("GOKZ_GetModeVersion");
+ MarkNativeAsOptional("GOKZ_SetModeLoaded");
+ MarkNativeAsOptional("GOKZ_GetLoadedModeCount");
+ MarkNativeAsOptional("GOKZ_SetMode");
+ MarkNativeAsOptional("GOKZ_GetOptionsTopMenu");
+ MarkNativeAsOptional("GOKZ_GetCourseRegistered");
+ MarkNativeAsOptional("GOKZ_PrintToChat");
+ MarkNativeAsOptional("GOKZ_PrintToChatAndLog");
+ MarkNativeAsOptional("GOKZ_StartTimer");
+ MarkNativeAsOptional("GOKZ_EndTimer");
+ MarkNativeAsOptional("GOKZ_StopTimer");
+ MarkNativeAsOptional("GOKZ_StopTimerAll");
+ MarkNativeAsOptional("GOKZ_TeleportToStart");
+ MarkNativeAsOptional("GOKZ_TeleportToSearchStart");
+ MarkNativeAsOptional("GOKZ_GetVirtualButtonPosition");
+ MarkNativeAsOptional("GOKZ_SetVirtualButtonPosition");
+ MarkNativeAsOptional("GOKZ_ResetVirtualButtonPosition");
+ MarkNativeAsOptional("GOKZ_LockVirtualButtons");
+ MarkNativeAsOptional("GOKZ_GetStartPosition");
+ MarkNativeAsOptional("GOKZ_SetStartPosition");
+ MarkNativeAsOptional("GOKZ_GetStartPositionType");
+ MarkNativeAsOptional("GOKZ_SetStartPositionToMapStart");
+ MarkNativeAsOptional("GOKZ_TeleportToEnd");
+ MarkNativeAsOptional("GOKZ_MakeCheckpoint");
+ MarkNativeAsOptional("GOKZ_GetCanMakeCheckpoint");
+ MarkNativeAsOptional("GOKZ_TeleportToCheckpoint");
+ MarkNativeAsOptional("GOKZ_GetCanTeleportToCheckpoint");
+ MarkNativeAsOptional("GOKZ_PrevCheckpoint");
+ MarkNativeAsOptional("GOKZ_GetCanPrevCheckpoint");
+ MarkNativeAsOptional("GOKZ_NextCheckpoint");
+ MarkNativeAsOptional("GOKZ_GetCanNextCheckpoint");
+ MarkNativeAsOptional("GOKZ_UndoTeleport");
+ MarkNativeAsOptional("GOKZ_GetCanUndoTeleport");
+ MarkNativeAsOptional("GOKZ_Pause");
+ MarkNativeAsOptional("GOKZ_GetCanPause");
+ MarkNativeAsOptional("GOKZ_Resume");
+ MarkNativeAsOptional("GOKZ_GetCanResume");
+ MarkNativeAsOptional("GOKZ_TogglePause");
+ MarkNativeAsOptional("GOKZ_GetCanTeleportToStartOrEnd");
+ MarkNativeAsOptional("GOKZ_PlayErrorSound");
+ MarkNativeAsOptional("GOKZ_SetValidJumpOrigin");
+ MarkNativeAsOptional("GOKZ_GetTimerRunning");
+ MarkNativeAsOptional("GOKZ_GetCourse");
+ MarkNativeAsOptional("GOKZ_SetCourse");
+ MarkNativeAsOptional("GOKZ_GetPaused");
+ MarkNativeAsOptional("GOKZ_GetTime");
+ MarkNativeAsOptional("GOKZ_SetTime");
+ MarkNativeAsOptional("GOKZ_InvalidateRun");
+ MarkNativeAsOptional("GOKZ_GetCheckpointCount");
+ MarkNativeAsOptional("GOKZ_SetCheckpointCount");
+ MarkNativeAsOptional("GOKZ_GetCheckpointData");
+ MarkNativeAsOptional("GOKZ_SetCheckpointData");
+ MarkNativeAsOptional("GOKZ_GetUndoTeleportData");
+ MarkNativeAsOptional("GOKZ_SetUndoTeleportData");
+ MarkNativeAsOptional("GOKZ_GetTeleportCount");
+ MarkNativeAsOptional("GOKZ_SetTeleportCount");
+ MarkNativeAsOptional("GOKZ_RegisterOption");
+ MarkNativeAsOptional("GOKZ_GetOptionProp");
+ MarkNativeAsOptional("GOKZ_SetOptionProp");
+ MarkNativeAsOptional("GOKZ_GetOption");
+ MarkNativeAsOptional("GOKZ_SetOption");
+ MarkNativeAsOptional("GOKZ_GetHitPerf");
+ MarkNativeAsOptional("GOKZ_SetHitPerf");
+ MarkNativeAsOptional("GOKZ_GetTakeoffSpeed");
+ MarkNativeAsOptional("GOKZ_SetTakeoffSpeed");
+ MarkNativeAsOptional("GOKZ_GetValidJump");
+ MarkNativeAsOptional("GOKZ_JoinTeam");
+ MarkNativeAsOptional("GOKZ_EmitSoundToClient");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/global.inc b/sourcemod/scripting/include/gokz/global.inc
new file mode 100644
index 0000000..0f23a0c
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/global.inc
@@ -0,0 +1,317 @@
+/*
+ gokz-global Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_global_included_
+#endinput
+#endif
+#define _gokz_global_included_
+
+#include <GlobalAPI>
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ EnforcedCVar_Cheats = 0,
+ EnforcedCVar_ClampUnsafeVelocities,
+ EnforcedCVar_DropKnifeEnable,
+ EnforcedCVar_AutoBunnyhopping,
+ EnforcedCVar_MinUpdateRate,
+ EnforcedCVar_MaxUpdateRate,
+ EnforcedCVar_MinCmdRate,
+ EnforcedCVar_MaxCmdRate,
+ EnforcedCVar_ClientCmdrateDifference,
+ EnforcedCVar_Turbophysics,
+ ENFORCEDCVAR_COUNT
+};
+
+enum
+{
+ BannedPluginCommand_Funcommands = 0,
+ BannedPluginCommand_Playercommands,
+ BANNEDPLUGINCOMMAND_COUNT
+};
+
+enum
+{
+ BannedPlugin_Funcommands = 0,
+ BannedPlugin_Playercommands,
+ BANNEDPLUGIN_COUNT
+};
+
+enum GlobalMode
+{
+ GlobalMode_Invalid = -1,
+ GlobalMode_KZTimer = 200,
+ GlobalMode_KZSimple,
+ GlobalMode_Vanilla
+}
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define GL_SOUND_NEW_RECORD "gokz/holyshit.mp3"
+#define GL_FPS_MAX_CHECK_INTERVAL 1.0
+#define GL_FPS_MAX_KICK_TIMEOUT 10.0
+#define GL_FPS_MAX_MIN_VALUE 120
+#define GL_MYAW_MAX_VALUE 0.3
+
+stock char gC_EnforcedCVars[ENFORCEDCVAR_COUNT][] =
+{
+ "sv_cheats",
+ "sv_clamp_unsafe_velocities",
+ "mp_drop_knife_enable",
+ "sv_autobunnyhopping",
+ "sv_minupdaterate",
+ "sv_maxupdaterate",
+ "sv_mincmdrate",
+ "sv_maxcmdrate",
+ "sv_client_cmdrate_difference",
+ "sv_turbophysics"
+};
+
+stock char gC_BannedPluginCommands[BANNEDPLUGINCOMMAND_COUNT][] =
+{
+ "sm_beacon",
+ "sm_slap"
+};
+
+stock char gC_BannedPlugins[BANNEDPLUGIN_COUNT][] =
+{
+ "Fun Commands",
+ "Player Commands"
+};
+
+stock float gF_EnforcedCVarValues[ENFORCEDCVAR_COUNT] =
+{
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 128.0,
+ 128.0,
+ 128.0,
+ 128.0,
+ 0.0,
+ 0.0
+};
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a player sets a new global top time.
+ *
+ * @param client Client index.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode Player's movement mode.
+ * @param timeType Time type i.e. NUB or PRO.
+ * @param rank Ranking within the same time type.
+ * @param rankOverall Overall (NUB and PRO) ranking (0 if not ranked high enough).
+ * @param runTime Player's end time.
+ */
+forward void GOKZ_GL_OnNewTopTime(int client, int course, int mode, int timeType, int rank, int rankOverall, float runTime);
+
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Prints to chat the global records for a map, course and mode.
+ *
+ * @param client Client index.
+ * @param map Map name or "" for current map.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode GOKZ mode.
+ */
+native void GOKZ_GL_PrintRecords(int client, const char[] map = "", int course, int mode, const char[] steamid = DEFAULT_STRING);
+
+/**
+ * Opens up the global map top menu for a map, course and mode.
+ *
+ * @param client Client index.
+ * @param map Map name or "" for current map.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode GOKZ mode.
+ * @param timeType Type of time i.e. NUB or PRO.
+ */
+native void GOKZ_GL_DisplayMapTopMenu(int client, const char[] map = "", int course, int mode, int timeType);
+
+/**
+ * Get the total global points of a player.
+ *
+ * @param client Client index.
+ * @param mode GOKZ mode.
+ * @param timeType Type of time i.e. NUB or PRO.
+ */
+native void GOKZ_GL_GetPoints(int client, int mode, int timeType);
+
+/**
+ * Get the global points on the main coruse of the current map.
+ *
+ * @param client Client index.
+ * @param mode GOKZ mode.
+ * @param timeType Type of time i.e. NUB or PRO.
+ */
+native void GOKZ_GL_GetMapPoints(int client, int mode, int timeType);
+
+/**
+ * Get the total global ranking points of a player.
+ *
+ * @param client Client index.
+ * @param mode GOKZ mode.
+ * @return The points.
+ */
+native int GOKZ_GL_GetRankPoints(int client, int mode);
+
+/**
+ * Get the amount of maps a player finished.
+ *
+ * @param client Client index.
+ * @param mode GOKZ mode.
+ * @param timeType Type of time i.e. NUB or PRO.
+ */
+native void GOKZ_GL_GetFinishes(int client, int mode, int timeType);
+
+/**
+ * Fetch the points a player got from the Global API.
+ *
+ * @param client Client index. -1 to update all indices.
+ * @param mode GOKZ mode. -1 to update all modes.
+ */
+native void GOKZ_GL_UpdatePoints(int client = -1, int mode = -1);
+
+/**
+ * Gets whether the Global API key is valid or not for global status.
+ *
+ * @return True if the API key is valid, false otherwise or if there is no connection to the Global API.
+ */
+native bool GOKZ_GL_GetAPIKeyValid();
+
+/**
+ * Gets whether the running plugins are valid or not for global status.
+ *
+ * @return True if the plugins are valid, false otherwise.
+ */
+native bool GOKZ_GL_GetPluginsValid();
+
+/**
+ * Gets whether the setting enforcer is valid or not for global status.
+ *
+ * @return True if the setting enforcer is valid, false otherwise.
+ */
+native bool GOKZ_GL_GetSettingsEnforcerValid();
+
+/**
+ * Gets whether the current map is valid or not for global status.
+ *
+ * @return True if the map is valid, false otherwise or if there is no connection to the Global API.
+ */
+native bool GOKZ_GL_GetMapValid();
+
+/**
+ * Gets whether the current player is valid or not for global status.
+ *
+ * @param client Client index.
+ * @return True if the player is valid, false otherwise or if there is no connection to the Global API.
+ */
+native bool GOKZ_GL_GetPlayerValid(int client);
+
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Gets the global mode enumeration equivalent for the GOKZ mode.
+ *
+ * @param mode GOKZ mode.
+ * @return Global mode enumeration equivalent.
+ */
+stock GlobalMode GOKZ_GL_GetGlobalMode(int mode)
+{
+ switch (mode)
+ {
+ case Mode_Vanilla:return GlobalMode_Vanilla;
+ case Mode_SimpleKZ:return GlobalMode_KZSimple;
+ case Mode_KZTimer:return GlobalMode_KZTimer;
+ }
+ return GlobalMode_Invalid;
+}
+
+/**
+ * Gets the global mode enumeration equivalent for the GOKZ mode.
+ *
+ * @param mode GOKZ mode.
+ * @return Global mode enumeration equivalent.
+ */
+stock int GOKZ_GL_FromGlobalMode(GlobalMode mode)
+{
+ switch (mode)
+ {
+ case GlobalMode_Vanilla:return Mode_Vanilla;
+ case GlobalMode_KZSimple:return Mode_SimpleKZ;
+ case GlobalMode_KZTimer:return Mode_KZTimer;
+ }
+ return -1;
+}
+
+/**
+ * Gets the string representation of a mode.
+ *
+ * @param mode GOKZ mode.
+ * @param mode_str String version of the mode.
+ * @param size Max length of mode_str.
+ * @return True if the conversion was successful.
+ */
+stock bool GOKZ_GL_GetModeString(int mode, char[] mode_str, int size)
+{
+ switch (mode)
+ {
+ case Mode_Vanilla:strcopy(mode_str, size, "kz_vanilla");
+ case Mode_SimpleKZ:strcopy(mode_str, size, "kz_simple");
+ case Mode_KZTimer:strcopy(mode_str, size, "kz_timer");
+ default:return false;
+ }
+ return true;
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_global =
+{
+ name = "gokz-global",
+ file = "gokz-global.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_global_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_GL_PrintRecords");
+ MarkNativeAsOptional("GOKZ_GL_DisplayMapTopMenu");
+ MarkNativeAsOptional("GOKZ_GL_UpdatePoints");
+ MarkNativeAsOptional("GOKZ_GL_GetAPIKeyValid");
+ MarkNativeAsOptional("GOKZ_GL_GetPluginsValid");
+ MarkNativeAsOptional("GOKZ_GL_GetSettingsEnforcerValid");
+ MarkNativeAsOptional("GOKZ_GL_GetMapValid");
+ MarkNativeAsOptional("GOKZ_GL_GetPlayerValid");
+ MarkNativeAsOptional("GOKZ_GL_GetPoints");
+ MarkNativeAsOptional("GOKZ_GL_GetMapPoints");
+ MarkNativeAsOptional("GOKZ_GL_GetRankPoints");
+ MarkNativeAsOptional("GOKZ_GL_GetFinishes");
+ MarkNativeAsOptional("GOKZ_GL_UpdatePoints");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/hud.inc b/sourcemod/scripting/include/gokz/hud.inc
new file mode 100644
index 0000000..5d658ff
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/hud.inc
@@ -0,0 +1,468 @@
+/*
+ gokz-hud Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_hud_included_
+#endinput
+#endif
+#define _gokz_hud_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum HUDOption:
+{
+ HUDOPTION_INVALID = -1,
+ HUDOption_TPMenu,
+ HUDOption_InfoPanel,
+ HUDOption_ShowKeys,
+ HUDOption_TimerText,
+ HUDOption_TimerStyle,
+ HUDOption_TimerType,
+ HUDOption_SpeedText,
+ HUDOption_ShowWeapon,
+ HUDOption_ShowControls,
+ HUDOption_DeadstrafeColor,
+ HUDOption_ShowSpectators,
+ HUDOption_SpecListPosition,
+ HUDOption_UpdateRate,
+ HUDOption_DynamicMenu,
+ HUDOPTION_COUNT
+};
+
+enum
+{
+ TPMenu_Disabled = 0,
+ TPMenu_Simple,
+ TPMenu_Advanced,
+ TPMENU_COUNT
+};
+
+enum
+{
+ InfoPanel_Disabled = 0,
+ InfoPanel_Enabled,
+ INFOPANEL_COUNT
+};
+
+enum
+{
+ ShowKeys_Spectating = 0,
+ ShowKeys_Always,
+ ShowKeys_Disabled,
+ SHOWKEYS_COUNT
+};
+
+enum
+{
+ TimerText_Disabled = 0,
+ TimerText_InfoPanel,
+ TimerText_TPMenu,
+ TimerText_Bottom,
+ TimerText_Top,
+ TIMERTEXT_COUNT
+};
+
+enum
+{
+ TimerStyle_Standard = 0,
+ TimerStyle_Precise,
+ TIMERSTYLE_COUNT
+};
+
+enum
+{
+ TimerType_Disabled = 0,
+ TimerType_Enabled,
+ TIMERTYPE_COUNT
+};
+
+enum
+{
+ SpeedText_Disabled = 0,
+ SpeedText_InfoPanel,
+ SpeedText_Bottom,
+ SPEEDTEXT_COUNT
+};
+
+enum
+{
+ ShowWeapon_Disabled = 0,
+ ShowWeapon_Enabled,
+ SHOWWEAPON_COUNT
+};
+
+enum
+{
+ ReplayControls_Disabled = 0,
+ ReplayControls_Enabled,
+ REPLAYCONTROLS_COUNT
+};
+
+enum
+{
+ DeadstrafeColor_Disabled = 0,
+ DeadstrafeColor_Enabled,
+ DEADSTRAFECOLOR_COUNT
+};
+
+enum
+{
+ ShowSpecs_Disabled = 0,
+ ShowSpecs_Number,
+ ShowSpecs_Full,
+ SHOWSPECS_COUNT
+};
+
+enum
+{
+ SpecListPosition_TPMenu = 0,
+ SpecListPosition_InfoPanel,
+ SPECLISTPOSITION_COUNT
+}
+
+enum
+{
+ UpdateRate_Slow = 0,
+ UpdateRate_Fast,
+ UPDATERATE_COUNT,
+};
+
+enum
+{
+ DynamicMenu_Legacy = 0,
+ DynamicMenu_Disabled,
+ DynamicMenu_Enabled,
+ DYNAMICMENU_COUNT
+};
+
+// =====[ STRUCTS ]======
+
+enum struct HUDInfo
+{
+ bool TimerRunning;
+ int TimeType;
+ float Time;
+ bool Paused;
+ bool OnGround;
+ bool OnLadder;
+ bool Noclipping;
+ bool Ducking;
+ bool HitBhop;
+ bool IsTakeoff;
+ float Speed;
+ int ID;
+ bool Jumped;
+ bool HitPerf;
+ bool HitJB;
+ float TakeoffSpeed;
+ int Buttons;
+ int CurrentTeleport;
+}
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define HUD_OPTION_CATEGORY "HUD"
+#define HUD_MAX_BHOP_GROUND_TICKS 5
+#define HUD_MAX_HINT_SIZE 227
+
+stock char gC_HUDOptionNames[HUDOPTION_COUNT][] =
+{
+ "GOKZ HUD - Teleport Menu",
+ "GOKZ HUD - Centre Panel",
+ "GOKZ HUD - Show Keys",
+ "GOKZ HUD - Timer Text",
+ "GOKZ HUD - Timer Style",
+ "GOKZ HUD - Show Time Type",
+ "GOKZ HUD - Speed Text",
+ "GOKZ HUD - Show Weapon",
+ "GOKZ HUD - Show Controls",
+ "GOKZ HUD - Dead Strafe",
+ "GOKZ HUD - Show Spectators",
+ "GOKZ HUD - Spec List Pos",
+ "GOKZ HUD - Update Rate",
+ "GOKZ HUD - Dynamic Menu"
+};
+
+stock char gC_HUDOptionDescriptions[HUDOPTION_COUNT][] =
+{
+ "Teleport Menu - 0 = Disabled, 1 = Simple, 2 = Advanced",
+ "Centre Information Panel - 0 = Disabled, 1 = Enabled",
+ "Key Press Display - 0 = Spectating, 1 = Always, 2 = Disabled",
+ "Timer Display - 0 = Disabled, 1 = Centre Panel, 2 = Teleport Menu, 3 = Bottom, 4 = Top",
+ "Timer Style - 0 = Standard, 1 = Precise",
+ "Timer Type - 0 = Disabled, 1 = Enabled",
+ "Speed Display - 0 = Disabled, 1 = Centre Panel, 2 = Bottom",
+ "Weapon Viewmodel - 0 = Disabled, 1 = Enabled",
+ "Replay Controls Display - 0 = Disbled, 1 = Enabled",
+ "Dead Strafe Indicator - 0 = Disabled, 1 = Enabled",
+ "Show Spectators - 0 = Disabled, 1 = Number Only, 2 = Number and Names",
+ "Spectator List Position - 0 = Teleport Menu, 2 = Center Panel",
+ "HUD Update Rate - 0 = Slow, 1 = Fast",
+ "Dynamic Menu - 0 = Legacy, 1 = Disabled, 2 = Enabled"
+};
+
+stock char gC_HUDOptionPhrases[HUDOPTION_COUNT][] =
+{
+ "Options Menu - Teleport Menu",
+ "Options Menu - Info Panel",
+ "Options Menu - Show Keys",
+ "Options Menu - Timer Text",
+ "Options Menu - Timer Style",
+ "Options Menu - Timer Type",
+ "Options Menu - Speed Text",
+ "Options Menu - Show Weapon",
+ "Options Menu - Show Controls",
+ "Options Menu - Dead Strafe Indicator",
+ "Options Menu - Show Spectators",
+ "Options Menu - Spectator List Position",
+ "Options Menu - Update Rate",
+ "Options Menu - Dynamic Menu"
+};
+
+stock int gI_HUDOptionCounts[HUDOPTION_COUNT] =
+{
+ TPMENU_COUNT,
+ INFOPANEL_COUNT,
+ SHOWKEYS_COUNT,
+ TIMERTEXT_COUNT,
+ TIMERSTYLE_COUNT,
+ TIMERTYPE_COUNT,
+ SPEEDTEXT_COUNT,
+ SHOWWEAPON_COUNT,
+ REPLAYCONTROLS_COUNT,
+ DEADSTRAFECOLOR_COUNT,
+ SHOWSPECS_COUNT,
+ SPECLISTPOSITION_COUNT,
+ UPDATERATE_COUNT,
+ DYNAMICMENU_COUNT
+};
+
+stock int gI_HUDOptionDefaults[HUDOPTION_COUNT] =
+{
+ TPMenu_Advanced,
+ InfoPanel_Enabled,
+ ShowKeys_Spectating,
+ TimerText_InfoPanel,
+ TimerStyle_Standard,
+ TimerType_Enabled,
+ SpeedText_InfoPanel,
+ ShowWeapon_Enabled,
+ ReplayControls_Enabled,
+ DeadstrafeColor_Disabled,
+ ShowSpecs_Disabled,
+ SpecListPosition_TPMenu,
+ UpdateRate_Slow,
+ DynamicMenu_Legacy
+};
+
+stock char gC_TPMenuPhrases[TPMENU_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Simple",
+ "Options Menu - Advanced"
+};
+
+stock char gC_ShowKeysPhrases[SHOWKEYS_COUNT][] =
+{
+ "Options Menu - Spectating",
+ "Options Menu - Always",
+ "Options Menu - Disabled"
+};
+
+stock char gC_TimerTextPhrases[TIMERTEXT_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Info Panel",
+ "Options Menu - Teleport Menu",
+ "Options Menu - Bottom",
+ "Options Menu - Top"
+};
+
+stock char gC_TimerTypePhrases[TIMERTYPE_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Enabled"
+};
+
+stock char gC_SpeedTextPhrases[SPEEDTEXT_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Info Panel",
+ "Options Menu - Bottom"
+};
+
+stock char gC_ShowControlsPhrases[REPLAYCONTROLS_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Enabled"
+};
+
+stock char gC_DeadstrafeColorPhrases[DEADSTRAFECOLOR_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Enabled"
+};
+
+stock char gC_ShowSpecsPhrases[SHOWSPECS_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Number",
+ "Options Menu - Number and Names"
+};
+
+stock char gC_SpecListPositionPhrases[SPECLISTPOSITION_COUNT][] =
+{
+ "Options Menu - Teleport Menu",
+ "Options Menu - Info Panel"
+};
+
+stock char gC_HUDUpdateRatePhrases[UPDATERATE_COUNT][]=
+{
+ "Options Menu - Slow",
+ "Options Menu - Fast"
+};
+
+stock char gC_DynamicMenuPhrases[DYNAMICMENU_COUNT][]=
+{
+ "Options Menu - Legacy",
+ "Options Menu - Disabled",
+ "Options Menu - Enabled"
+};
+
+// =====[ NATIVES ]=====
+
+/**
+ * Returns whether the GOKZ HUD menu is showing for a client.
+ *
+ * @param client Client index.
+ * @return Whether the GOKZ HUD menu is showing.
+ */
+native bool GOKZ_HUD_GetMenuShowing(int client);
+
+/**
+ * Sets whether the GOKZ HUD menu would be showing for a client.
+ *
+ * @param client Client index.
+ * @param value Whether the GOKZ HUD menu would be showing for a client.
+ */
+native void GOKZ_HUD_SetMenuShowing(int client, bool value);
+
+/**
+ * Gets the spectator text for the menu. Used by GOKZ-replays.
+ *
+ * @param client Client index.
+ * @param value Whether the GOKZ HUD menu would be showing for a client.
+ */
+native void GOKZ_HUD_GetMenuSpectatorText(int client, any[] info, char[] buffer, int size);
+
+/**
+ * Forces the client's TP menu to update.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_HUD_ForceUpdateTPMenu(int client);
+
+// =====[ STOCKS ]=====
+
+/**
+ * Returns whether an option is a gokz-hud option.
+ *
+ * @param option Option name.
+ * @param optionEnum Variable to store enumerated gokz-hud option (if it is one).
+ * @return Whether option is a gokz-hud option.
+ */
+stock bool GOKZ_HUD_IsHUDOption(const char[] option, HUDOption &optionEnum = HUDOPTION_INVALID)
+{
+ for (HUDOption i; i < HUDOPTION_COUNT; i++)
+ {
+ if (StrEqual(option, gC_HUDOptionNames[i]))
+ {
+ optionEnum = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Gets the current value of a player's gokz-hud option.
+ *
+ * @param client Client index.
+ * @param option gokz-hud option.
+ * @return Current value of option.
+ */
+stock any GOKZ_HUD_GetOption(int client, HUDOption option)
+{
+ return GOKZ_GetOption(client, gC_HUDOptionNames[option]);
+}
+
+/**
+ * Sets a player's gokz-hud option's value.
+ *
+ * @param client Client index.
+ * @param option gokz-hud option.
+ * @param value New option value.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_HUD_SetOption(int client, HUDOption option, any value)
+{
+ return GOKZ_SetOption(client, gC_HUDOptionNames[option], value);
+}
+
+/**
+ * Increment an integer-type gokz-hud option's value.
+ * Loops back to '0' if max value is exceeded.
+ *
+ * @param client Client index.
+ * @param option gokz-hud option.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_HUD_CycleOption(int client, HUDOption option)
+{
+ return GOKZ_CycleOption(client, gC_HUDOptionNames[option]);
+}
+
+/**
+ * Represents a time float as a string e.g. 01:23.45
+ * and according to the client's HUD options.
+ *
+ * @param client Client index.
+ * @param time Time in seconds.
+ * @return String representation of time.
+ */
+stock char[] GOKZ_HUD_FormatTime(int client, float time)
+{
+ bool precise = GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Precise;
+ return GOKZ_FormatTime(time, precise);
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_hud =
+{
+ name = "gokz-hud",
+ file = "gokz-hud.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_hud_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_HUD_GetMenuShowing");
+ MarkNativeAsOptional("GOKZ_HUD_SetMenuShowing");
+ MarkNativeAsOptional("GOKZ_HUD_GetMenuSpectatorText");
+ MarkNativeAsOptional("GOKZ_HUD_ForceUpdateTPMenu");
+}
+#endif
diff --git a/sourcemod/scripting/include/gokz/jumpbeam.inc b/sourcemod/scripting/include/gokz/jumpbeam.inc
new file mode 100644
index 0000000..1b92479
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/jumpbeam.inc
@@ -0,0 +1,148 @@
+/*
+ gokz-jumpbeam Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_jumpbeam_included_
+#endinput
+#endif
+#define _gokz_jumpbeam_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum JBOption:
+{
+ JBOPTION_INVALID = -1,
+ JBOption_Type,
+ JBOPTION_COUNT
+};
+
+enum
+{
+ JBType_Disabled = 0,
+ JBType_Feet,
+ JBType_Head,
+ JBType_FeetAndHead,
+ JBType_Ground,
+ JBTYPE_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define JB_BEAM_LIFETIME 4.0
+
+stock char gC_JBOptionNames[JBOPTION_COUNT][] =
+{
+ "GOKZ JB - Jump Beam Type"
+};
+
+stock char gC_JBOptionDescriptions[JBOPTION_COUNT][] =
+{
+ "Jump Beam Type - 0 = Disabled, 1 = Feet, 2 = Head, 3 = Feet & Head, 4 = Ground"
+};
+
+stock int gI_JBOptionDefaultValues[JBOPTION_COUNT] =
+{
+ JBType_Disabled
+};
+
+stock int gI_JBOptionCounts[JBOPTION_COUNT] =
+{
+ JBTYPE_COUNT
+};
+
+stock char gC_JBOptionPhrases[JBOPTION_COUNT][] =
+{
+ "Options Menu - Jump Beam"
+};
+
+stock char gC_JBTypePhrases[JBTYPE_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Feet",
+ "Options Menu - Head",
+ "Options Menu - Feet and Head",
+ "Options Menu - Ground"
+};
+
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Returns whether an option is a gokz-jumpbeam option.
+ *
+ * @param option Option name.
+ * @param optionEnum Variable to store enumerated gokz-jumpbeam option (if it is one).
+ * @return Whether option is a gokz-jumpbeam option.
+ */
+stock bool GOKZ_JB_IsJBOption(const char[] option, JBOption &optionEnum = JBOPTION_INVALID)
+{
+ for (JBOption i; i < JBOPTION_COUNT; i++)
+ {
+ if (StrEqual(option, gC_JBOptionNames[i]))
+ {
+ optionEnum = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Gets the current value of a player's gokz-jumpbeam option.
+ *
+ * @param client Client index.
+ * @param option gokz-jumpbeam option.
+ * @return Current value of option.
+ */
+stock any GOKZ_JB_GetOption(int client, JBOption option)
+{
+ return GOKZ_GetOption(client, gC_JBOptionNames[option]);
+}
+
+/**
+ * Sets a player's gokz-jumpbeam option's value.
+ *
+ * @param client Client index.
+ * @param option gokz-jumpbeam option.
+ * @param value New option value.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_JB_SetOption(int client, JBOption option, any value)
+{
+ return GOKZ_SetOption(client, gC_JBOptionNames[option], value);
+}
+
+/**
+ * Increment an integer-type gokz-jumpbeam option's value.
+ * Loops back to '0' if max value is exceeded.
+ *
+ * @param client Client index.
+ * @param option gokz-jumpbeam option.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_JB_CycleOption(int client, JBOption option)
+{
+ return GOKZ_CycleOption(client, gC_JBOptionNames[option]);
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_jumpbeam =
+{
+ name = "gokz-jumpbeam",
+ file = "gokz-jumpbeam.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/jumpstats.inc b/sourcemod/scripting/include/gokz/jumpstats.inc
new file mode 100644
index 0000000..452ae28
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/jumpstats.inc
@@ -0,0 +1,442 @@
+/*
+ gokz-jumpstats Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_jumpstats_included_
+#endinput
+#endif
+#define _gokz_jumpstats_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ JumpType_FullInvalid = -1,
+ JumpType_LongJump,
+ JumpType_Bhop,
+ JumpType_MultiBhop,
+ JumpType_WeirdJump,
+ JumpType_LadderJump,
+ JumpType_Ladderhop,
+ JumpType_Jumpbug,
+ JumpType_LowpreBhop,
+ JumpType_LowpreWeirdJump,
+ JumpType_Fall,
+ JumpType_Other,
+ JumpType_Invalid,
+ JUMPTYPE_COUNT
+};
+
+enum
+{
+ StrafeDirection_None,
+ StrafeDirection_Left,
+ StrafeDirection_Right
+};
+
+enum
+{
+ DistanceTier_None = 0,
+ DistanceTier_Meh,
+ DistanceTier_Impressive,
+ DistanceTier_Perfect,
+ DistanceTier_Godlike,
+ DistanceTier_Ownage,
+ DistanceTier_Wrecker,
+ DISTANCETIER_COUNT
+};
+
+enum JSOption:
+{
+ JSOPTION_INVALID = -1,
+ JSOption_JumpstatsMaster,
+ JSOption_MinChatTier,
+ JSOption_MinConsoleTier,
+ JSOption_MinSoundTier,
+ JSOption_FailstatsConsole,
+ JSOption_FailstatsChat,
+ JSOption_JumpstatsAlways,
+ JSOption_ExtendedChatReport,
+ JSOption_MinChatBroadcastTier,
+ JSOption_MinSoundBroadcastTier,
+ JSOPTION_COUNT
+};
+
+enum
+{
+ JSToggleOption_Disabled = 0,
+ JSToggleOption_Enabled,
+ JSTOGGLEOPTION_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define JS_CFG_TIERS "cfg/sourcemod/gokz/gokz-jumpstats-tiers.cfg"
+#define JS_CFG_SOUNDS "cfg/sourcemod/gokz/gokz-jumpstats-sounds.cfg"
+#define JS_CFG_BROADCAST "cfg/sourcemod/gokz/gokz-jumpstats-broadcast.cfg"
+#define JS_OPTION_CATEGORY "Jumpstats"
+#define JS_MAX_LADDERJUMP_OFFSET 2.0
+#define JS_MAX_BHOP_GROUND_TICKS 5
+#define JS_MAX_DUCKBUG_RESET_TICKS 6
+#define JS_MAX_WEIRDJUMP_FALL_OFFSET 64.0
+#define JS_TOUCH_GRACE_TICKS 3
+#define JS_MAX_TRACKED_STRAFES 48
+#define JS_MIN_BLOCK_DISTANCE 186
+#define JS_MIN_LAJ_BLOCK_DISTANCE 50
+#define JS_MAX_LAJ_FAILSTAT_DISTANCE 250
+#define JS_TOP_RECORD_COUNT 20
+#define JS_MAX_JUMP_DISTANCE 500
+#define JS_FAILSTATS_MAX_TRACKED_TICKS 128
+#define JS_MIN_TELEPORT_DELAY 5
+#define JS_SPEED_MODIFICATION_TOLERANCE 0.1
+#define JS_OFFSET_EPSILON 0.03125
+
+stock char gC_JumpTypes[JUMPTYPE_COUNT][] =
+{
+ "Long Jump",
+ "Bunnyhop",
+ "Multi Bunnyhop",
+ "Weird Jump",
+ "Ladder Jump",
+ "Ladderhop",
+ "Jumpbug",
+ "Lowpre Bunnyhop",
+ "Lowpre Weird Jump",
+ "Fall",
+ "Unknown Jump",
+ "Invalid Jump"
+};
+
+stock char gC_JumpTypesShort[JUMPTYPE_COUNT][] =
+{
+ "LJ",
+ "BH",
+ "MBH",
+ "WJ",
+ "LAJ",
+ "LAH",
+ "JB",
+ "LBH",
+ "LWJ",
+ "FL",
+ "UNK",
+ "INV"
+};
+
+stock char gC_JumpTypeKeys[JUMPTYPE_COUNT][] =
+{
+ "longjump",
+ "bhop",
+ "multibhop",
+ "weirdjump",
+ "ladderjump",
+ "ladderhop",
+ "jumpbug",
+ "lowprebhop",
+ "lowpreweirdjump",
+ "fall",
+ "unknown",
+ "invalid"
+};
+
+stock char gC_DistanceTiers[DISTANCETIER_COUNT][] =
+{
+ "None",
+ "Meh",
+ "Impressive",
+ "Perfect",
+ "Godlike",
+ "Ownage",
+ "Wrecker"
+};
+
+stock char gC_DistanceTierKeys[DISTANCETIER_COUNT][] =
+{
+ "none",
+ "meh",
+ "impressive",
+ "perfect",
+ "godlike",
+ "ownage",
+ "wrecker"
+};
+
+stock char gC_DistanceTierChatColours[DISTANCETIER_COUNT][] =
+{
+ "{grey}",
+ "{grey}",
+ "{blue}",
+ "{green}",
+ "{darkred}",
+ "{gold}",
+ "{orchid}"
+};
+
+stock char gC_JSOptionNames[JSOPTION_COUNT][] =
+{
+ "GOKZ JS - Master Switch",
+ "GOKZ JS - Chat Report",
+ "GOKZ JS - Console Report",
+ "GOKZ JS - Sounds",
+ "GOKZ JS - Failstats Console",
+ "GOKZ JS - Failstats Chat",
+ "GOKZ JS - Jumpstats Always",
+ "GOKZ JS - Ext Chat Report",
+ "GOKZ JS - Min Chat Broadcast",
+ "GOKZ JS - Min Sound Broadcast"
+};
+
+stock char gC_JSOptionDescriptions[JSOPTION_COUNT][] =
+{
+ "Master Switch for All Jumpstats Functionality - 0 = Disabled, 1 = Enabled",
+ "Minimum Tier for Jumpstats Chat Report - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker",
+ "Minimum Tier for Jumpstats Console report - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker",
+ "Minimum Tier for Jumpstats Sounds - 0 = Disabled, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker",
+ "Print Failstats To Console - 0 = Disabled, 1 = Enabled",
+ "Print Failstats To Chat - 0 = Disabled, 1 = Enabled",
+ "Always show jumpstats, even for invalid jumps - 0 = Disabled, 1 = Enabled",
+ "Extended Chat Report - 0 = Disabled, 1 = Enabled",
+ "Minimum Jump Tier for Jumpstat Chat Broadcast - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker",
+ "Minimum Jump Tier for Jumpstat Sound Broadcast - 0 = Disabled, 1 = Meh+, 2 = Impressive+, 3 = Perfect+, 4 = Godlike+, 5 = Ownage+, 6 = Wrecker"
+};
+
+stock char gI_JSOptionPhrases[JSOPTION_COUNT][] =
+{
+ "Options Menu - Jumpstats Master Switch",
+ "Options Menu - Jumpstats Chat Report",
+ "Options Menu - Jumpstats Console Report",
+ "Options Menu - Jumpstats Sounds",
+ "Options Menu - Failstats Console Report",
+ "Options Menu - Failstats Chat Report",
+ "Options Menu - Jumpstats Always",
+ "Options Menu - Extended Jump Chat Report",
+ "Options Menu - Minimal Jump Chat Broadcast Tier",
+ "Options Menu - Minimal Jump Sound Broadcast Tier"
+};
+
+stock int gI_JSOptionDefaults[JSOPTION_COUNT] =
+{
+ JSToggleOption_Enabled,
+ DistanceTier_Meh,
+ DistanceTier_Meh,
+ DistanceTier_Impressive,
+ JSToggleOption_Enabled,
+ JSToggleOption_Disabled,
+ JSToggleOption_Disabled,
+ JSToggleOption_Disabled,
+ DistanceTier_Ownage,
+ DistanceTier_None
+};
+
+stock int gI_JSOptionCounts[JSOPTION_COUNT] =
+{
+ JSTOGGLEOPTION_COUNT,
+ DISTANCETIER_COUNT,
+ DISTANCETIER_COUNT,
+ DISTANCETIER_COUNT,
+ JSTOGGLEOPTION_COUNT,
+ JSTOGGLEOPTION_COUNT,
+ JSTOGGLEOPTION_COUNT,
+ JSTOGGLEOPTION_COUNT,
+ DISTANCETIER_COUNT,
+ DISTANCETIER_COUNT
+};
+
+
+
+// =====[ STRUCTS ]=====
+
+enum struct Jump
+{
+ int jumper;
+ int block;
+ int crouchRelease;
+ int crouchTicks;
+ int deadair;
+ int duration;
+ int originalType;
+ int overlap;
+ int releaseW;
+ int strafes;
+ int type;
+ float deviation;
+ float distance;
+ float edge;
+ float height;
+ float maxSpeed;
+ float offset;
+ float preSpeed;
+ float sync;
+ float width;
+
+ // For the 'always' stats
+ float miss;
+
+ // We can't make a separate enum struct for that cause it won't let us
+ // index an array of enum structs.
+ int strafes_gainTicks[JS_MAX_TRACKED_STRAFES];
+ int strafes_deadair[JS_MAX_TRACKED_STRAFES];
+ int strafes_overlap[JS_MAX_TRACKED_STRAFES];
+ int strafes_ticks[JS_MAX_TRACKED_STRAFES];
+ float strafes_gain[JS_MAX_TRACKED_STRAFES];
+ float strafes_loss[JS_MAX_TRACKED_STRAFES];
+ float strafes_sync[JS_MAX_TRACKED_STRAFES];
+ float strafes_width[JS_MAX_TRACKED_STRAFES];
+}
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a player begins their jump.
+ *
+ * @param client Client index.
+ * @param jumpType Type of jump.
+ */
+forward void GOKZ_JS_OnTakeoff(int client, int jumpType);
+
+/**
+ * Called when a player lands their jump.
+ *
+ * @param jump The jumpstats.
+ */
+forward void GOKZ_JS_OnLanding(Jump jump);
+
+/**
+ * Called when player's current jump has been declared an invalid jumpstat.
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_JS_OnJumpInvalidated(int client);
+
+/**
+ * Called when a player fails a blockjump.
+ *
+ * @param jump The jumpstats.
+ */
+forward void GOKZ_JS_OnFailstat(Jump jump);
+
+/**
+ * Called when a player lands a jump and has always-on jumpstats enabled.
+ *
+ * @param jump The jumpstats.
+ */
+forward void GOKZ_JS_OnJumpstatAlways(Jump jump);
+
+/**
+ * Called when a player fails a jump and has always-on failstats enabled.
+ *
+ * @param jump The failstats.
+ */
+forward void GOKZ_JS_OnFailstatAlways(Jump jump);
+
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets the default jumpstats option value as set by a config file.
+ *
+ * @param option GOKZ Jumpstats option.
+ * @return Default option value.
+ */
+native int GOKZ_JS_GetDefaultOption(JSOption option);
+
+/**
+ * Declare a player's current jump an invalid jumpstat.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_JS_InvalidateJump(int client);
+
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Returns whether an option is a gokz-jumpstats option.
+ *
+ * @param option Option name.
+ * @param optionEnum Variable to store enumerated gokz-jumpstats option (if it is one).
+ * @return Whether option is a gokz-jumpstats option.
+ */
+stock bool GOKZ_JS_IsJSOption(const char[] option, JSOption &optionEnum = JSOPTION_INVALID)
+{
+ for (JSOption i; i < JSOPTION_COUNT; i++)
+ {
+ if (StrEqual(option, gC_JSOptionNames[i]))
+ {
+ optionEnum = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Gets the current value of a player's gokz-jumpstats option.
+ *
+ * @param client Client index.
+ * @param option gokz-jumpstats option.
+ * @return Current value of option.
+ */
+stock any GOKZ_JS_GetOption(int client, JSOption option)
+{
+ return GOKZ_GetOption(client, gC_JSOptionNames[option]);
+}
+
+/**
+ * Sets a player's gokz-jumpstats option's value.
+ *
+ * @param client Client index.
+ * @param option gokz-jumpstats option.
+ * @param value New option value.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_JS_SetOption(int client, JSOption option, any value)
+{
+ return GOKZ_SetOption(client, gC_JSOptionNames[option], value);
+}
+
+/**
+ * Increment an integer-type gokz-jumpstats option's value.
+ * Loops back to '0' if max value is exceeded.
+ *
+ * @param client Client index.
+ * @param option gokz-jumpstats option.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_JS_CycleOption(int client, JSOption option)
+{
+ return GOKZ_CycleOption(client, gC_JSOptionNames[option]);
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_jumpstats =
+{
+ name = "gokz-jumpstats",
+ file = "gokz-jumpstats.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_jumpstats_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_JS_GetDefaultOption");
+ MarkNativeAsOptional("GOKZ_JS_InvalidateJump");
+}
+#endif
diff --git a/sourcemod/scripting/include/gokz/kzplayer.inc b/sourcemod/scripting/include/gokz/kzplayer.inc
new file mode 100644
index 0000000..8176d39
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/kzplayer.inc
@@ -0,0 +1,584 @@
+/*
+ GOKZ KZPlayer Methodmap Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_kzplayer_included_
+#endinput
+#endif
+#define _gokz_kzplayer_included_
+
+#include <movementapi>
+
+#include <gokz>
+
+
+
+methodmap KZPlayer < MovementAPIPlayer {
+
+ public KZPlayer(int client) {
+ return view_as<KZPlayer>(MovementAPIPlayer(client));
+ }
+
+
+
+ // =====[ GENERAL ]=====
+
+ property bool Valid {
+ public get() {
+ return IsValidClient(this.ID);
+ }
+ }
+
+ property bool InGame {
+ public get() {
+ return IsClientInGame(this.ID);
+ }
+ }
+
+ property bool Authorized {
+ public get() {
+ return IsClientAuthorized(this.ID);
+ }
+ }
+
+ property bool Fake {
+ public get() {
+ return IsFakeClient(this.ID);
+ }
+ }
+
+ property bool Alive {
+ public get() {
+ return IsPlayerAlive(this.ID);
+ }
+ }
+
+ property ObsMode ObserverMode {
+ public get() {
+ return GetObserverMode(this.ID);
+ }
+ }
+
+ property int ObserverTarget {
+ public get() {
+ return GetObserverTarget(this.ID);
+ }
+ }
+
+
+
+ // =====[ CORE ]=====
+ #if defined _gokz_core_included_
+
+ public void StartTimer(int course) {
+ GOKZ_StartTimer(this.ID, course);
+ }
+
+ public void EndTimer(int course) {
+ GOKZ_EndTimer(this.ID, course);
+ }
+
+ public bool StopTimer() {
+ return GOKZ_StopTimer(this.ID);
+ }
+
+ public void TeleportToStart() {
+ GOKZ_TeleportToStart(this.ID);
+ }
+
+ public void TeleportToSearchStart(int course) {
+ GOKZ_TeleportToSearchStart(this.ID, course);
+ }
+
+ public void TeleportToEnd(int course) {
+ GOKZ_TeleportToEnd(this.ID, course);
+ }
+
+ property StartPositionType StartPositionType {
+ public get() {
+ return GOKZ_GetStartPositionType(this.ID);
+ }
+ }
+
+ public void MakeCheckpoint() {
+ GOKZ_MakeCheckpoint(this.ID);
+ }
+
+ property bool CanMakeCheckpoint {
+ public get() {
+ return GOKZ_GetCanMakeCheckpoint(this.ID);
+ }
+ }
+
+ public void TeleportToCheckpoint() {
+ GOKZ_TeleportToCheckpoint(this.ID);
+ }
+
+ property bool CanTeleportToCheckpoint {
+ public get() {
+ return GOKZ_GetCanTeleportToCheckpoint(this.ID);
+ }
+ }
+
+ public void PrevCheckpoint() {
+ GOKZ_PrevCheckpoint(this.ID);
+ }
+
+ property bool CanPrevCheckpoint {
+ public get() {
+ return GOKZ_GetCanPrevCheckpoint(this.ID);
+ }
+ }
+
+ public void NextCheckpoint() {
+ GOKZ_NextCheckpoint(this.ID);
+ }
+
+ property bool CanNextCheckpoint {
+ public get() {
+ return GOKZ_GetCanNextCheckpoint(this.ID);
+ }
+ }
+
+ public void UndoTeleport() {
+ GOKZ_UndoTeleport(this.ID);
+ }
+
+ property bool CanUndoTeleport {
+ public get() {
+ return GOKZ_GetCanUndoTeleport(this.ID);
+ }
+ }
+
+ public void Pause() {
+ GOKZ_Pause(this.ID);
+ }
+
+ property bool CanPause {
+ public get() {
+ return GOKZ_GetCanPause(this.ID);
+ }
+ }
+
+ public void Resume() {
+ GOKZ_Resume(this.ID);
+ }
+
+ property bool CanResume {
+ public get() {
+ return GOKZ_GetCanResume(this.ID);
+ }
+ }
+
+ public void TogglePause() {
+ GOKZ_TogglePause(this.ID);
+ }
+
+ public void PlayErrorSound() {
+ GOKZ_PlayErrorSound(this.ID);
+ }
+
+ property bool TimerRunning {
+ public get() {
+ return GOKZ_GetTimerRunning(this.ID);
+ }
+ }
+
+ property int Course {
+ public get() {
+ return GOKZ_GetCourse(this.ID);
+ }
+ }
+
+ property bool Paused {
+ public get() {
+ return GOKZ_GetPaused(this.ID);
+ }
+ public set(bool pause) {
+ if (pause) {
+ this.Pause();
+ }
+ else {
+ this.Resume();
+ }
+ }
+ }
+
+ property bool CanTeleportToStart {
+ public get() {
+ return GOKZ_GetCanTeleportToStartOrEnd(this.ID);
+ }
+ }
+
+ property float Time {
+ public get() {
+ return GOKZ_GetTime(this.ID);
+ }
+ public set(float value) {
+ GOKZ_SetTime(this.ID, value);
+ }
+ }
+
+ property int CheckpointCount {
+ public get() {
+ return GOKZ_GetCheckpointCount(this.ID);
+ }
+ public set(int cpCount) {
+ GOKZ_SetCheckpointCount(this.ID, cpCount);
+ }
+ }
+
+ property ArrayList CheckpointData {
+ public get() {
+ return GOKZ_GetCheckpointData(this.ID);
+ }
+ public set(ArrayList checkpoints) {
+ GOKZ_SetCheckpointData(this.ID, checkpoints, GOKZ_CHECKPOINT_VERSION);
+ }
+ }
+
+ property int TeleportCount {
+ public get() {
+ return GOKZ_GetTeleportCount(this.ID);
+ }
+ public set(int value) {
+ GOKZ_SetTeleportCount(this.ID, value);
+ }
+ }
+
+ property int TimeType {
+ public get() {
+ return GOKZ_GetTimeType(this.ID);
+ }
+ }
+
+ property bool GOKZHitPerf {
+ public get() {
+ return GOKZ_GetHitPerf(this.ID);
+ }
+ public set(bool value) {
+ GOKZ_SetHitPerf(this.ID, value);
+ }
+ }
+
+ property float GOKZTakeoffSpeed {
+ public get() {
+ return GOKZ_GetTakeoffSpeed(this.ID);
+ }
+ public set(float value) {
+ GOKZ_SetTakeoffSpeed(this.ID, value);
+ }
+ }
+
+ property bool ValidJump {
+ public get() {
+ return GOKZ_GetValidJump(this.ID);
+ }
+ }
+
+ public any GetOption(const char[] option) {
+ return GOKZ_GetOption(this.ID, option);
+ }
+
+ public bool SetOption(const char[] option, any value) {
+ return GOKZ_SetOption(this.ID, option, value);
+ }
+
+ public bool CycleOption(const char[] option) {
+ return GOKZ_CycleOption(this.ID, option);
+ }
+
+ public any GetCoreOption(Option option) {
+ return GOKZ_GetCoreOption(this.ID, option);
+ }
+
+ public bool SetCoreOption(Option option, int value) {
+ return GOKZ_SetCoreOption(this.ID, option, value);
+ }
+
+ public bool CycleCoreOption(Option option) {
+ return GOKZ_CycleCoreOption(this.ID, option);
+ }
+
+ property int Mode {
+ public get() {
+ return this.GetCoreOption(Option_Mode);
+ }
+ public set(int value) {
+ this.SetCoreOption(Option_Mode, value);
+ }
+ }
+
+ property int Style {
+ public get() {
+ return this.GetCoreOption(Option_Style);
+ }
+ public set(int value) {
+ this.SetCoreOption(Option_Style, value);
+ }
+ }
+
+ property int CheckpointMessages {
+ public get() {
+ return this.GetCoreOption(Option_CheckpointMessages);
+ }
+ public set(int value) {
+ this.SetCoreOption(Option_CheckpointMessages, value);
+ }
+ }
+
+ property int CheckpointSounds {
+ public get() {
+ return this.GetCoreOption(Option_CheckpointSounds);
+ }
+ public set(int value) {
+ this.SetCoreOption(Option_CheckpointSounds, value);
+ }
+ }
+
+ property int TeleportSounds {
+ public get() {
+ return this.GetCoreOption(Option_TeleportSounds);
+ }
+ public set(int value) {
+ this.SetCoreOption(Option_TeleportSounds, value);
+ }
+ }
+
+ property int ErrorSounds {
+ public get() {
+ return this.GetCoreOption(Option_ErrorSounds);
+ }
+ public set(int value) {
+ this.SetCoreOption(Option_ErrorSounds, value);
+ }
+ }
+
+ #endif
+ // =====[ END CORE ]=====
+
+
+
+ // =====[ HUD ]=====
+ #if defined _gokz_hud_included_
+
+ public any GetHUDOption(HUDOption option) {
+ return GOKZ_HUD_GetOption(this.ID, option);
+ }
+
+ public bool SetHUDOption(HUDOption option, any value) {
+ return GOKZ_HUD_SetOption(this.ID, option, value);
+ }
+
+ public bool CycleHUDOption(HUDOption option) {
+ return GOKZ_HUD_CycleOption(this.ID, option);
+ }
+
+ property int TPMenu {
+ public get() {
+ return this.GetHUDOption(HUDOption_TPMenu);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_TPMenu, value);
+ }
+ }
+
+ property int InfoPanel {
+ public get() {
+ return this.GetHUDOption(HUDOption_InfoPanel);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_InfoPanel, value);
+ }
+ }
+
+ property int ShowKeys {
+ public get() {
+ return this.GetHUDOption(HUDOption_ShowKeys);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_ShowKeys, value);
+ }
+ }
+
+ property int TimerText {
+ public get() {
+ return this.GetHUDOption(HUDOption_TimerText);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_TimerText, value);
+ }
+ }
+
+ property int TimerStyle {
+ public get() {
+ return this.GetHUDOption(HUDOption_TimerStyle);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_TimerStyle, value);
+ }
+ }
+
+ property int SpeedText {
+ public get() {
+ return this.GetHUDOption(HUDOption_SpeedText);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_SpeedText, value);
+ }
+ }
+
+ property int ShowWeapon {
+ public get() {
+ return this.GetHUDOption(HUDOption_ShowWeapon);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_ShowWeapon, value);
+ }
+ }
+
+ property int ReplayControls {
+ public get() {
+ return this.GetHUDOption(HUDOption_ShowControls);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_ShowControls, value);
+ }
+ }
+
+ property int ShowSpectators {
+ public get() {
+ return this.GetHUDOption(HUDOption_ShowSpectators);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_ShowSpectators, value);
+ }
+ }
+
+ property int SpecListPosition {
+ public get() {
+ return this.GetHUDOption(HUDOption_SpecListPosition);
+ }
+ public set(int value){
+ this.SetHUDOption(HUDOption_SpecListPosition, value);
+ }
+ }
+
+ property bool MenuShowing {
+ public get() {
+ return GOKZ_HUD_GetMenuShowing(this.ID);
+ }
+ public set(bool value) {
+ GOKZ_HUD_SetMenuShowing(this.ID, value);
+ }
+ }
+ property int DynamicMenu {
+ public get() {
+ return this.GetHUDOption(HUDOption_DynamicMenu);
+ }
+ public set(int value) {
+ this.SetHUDOption(HUDOption_DynamicMenu, value);
+ }
+ }
+ #endif
+ // =====[ END HUD ]=====
+
+
+
+ // =====[ PISTOL ]=====
+ #if defined _gokz_pistol_included_
+
+ property int Pistol {
+ public get() {
+ return this.GetOption(PISTOL_OPTION_NAME);
+ }
+ public set(int value) {
+ this.SetOption(PISTOL_OPTION_NAME, value);
+ }
+ }
+
+ #endif
+ // =====[ END PISTOL ]=====
+
+
+
+ // =====[ JUMP BEAM ]=====
+ #if defined _gokz_jumpbeam_included_
+
+ public any GetJBOption(JBOption option) {
+ return GOKZ_JB_GetOption(this.ID, option);
+ }
+
+ public bool SetJBOption(JBOption option, any value) {
+ return GOKZ_JB_SetOption(this.ID, option, value);
+ }
+
+ public bool CycleJBOption(JBOption option) {
+ return GOKZ_JB_CycleOption(this.ID, option);
+ }
+
+ property int JBType {
+ public get() {
+ return this.GetJBOption(JBOption_Type);
+ }
+ public set(int value) {
+ this.SetJBOption(JBOption_Type, value);
+ }
+ }
+
+ #endif
+ // =====[ END JUMP BEAM ]=====
+
+
+
+ // =====[ TIPS ]=====
+ #if defined _gokz_tips_included_
+
+ property int Tips {
+ public get() {
+ return this.GetOption(TIPS_OPTION_NAME);
+ }
+ public set(int value) {
+ this.SetOption(TIPS_OPTION_NAME, value);
+ }
+ }
+
+ #endif
+ // =====[ END TIPS ]=====
+
+
+
+ // =====[ QUIET ]=====
+ #if defined _gokz_quiet_included_
+
+ property int ShowPlayers {
+ public get() {
+ return this.GetOption(gC_QTOptionNames[QTOption_ShowPlayers]);
+ }
+ public set(int value) {
+ this.SetOption(gC_QTOptionNames[QTOption_ShowPlayers], value);
+ }
+ }
+
+ #endif
+ // =====[ END QUIET ]=====
+
+
+
+ // =====[ SLAY ON END ]=====
+ #if defined _gokz_slayonend_included_
+
+ property int SlayOnEnd {
+ public get() {
+ return this.GetOption(SLAYONEND_OPTION_NAME);
+ }
+ public set(int value) {
+ this.SetOption(SLAYONEND_OPTION_NAME, value);
+ }
+ }
+
+ #endif
+ // =====[ END SLAY ON END ]=====
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/localdb.inc b/sourcemod/scripting/include/gokz/localdb.inc
new file mode 100644
index 0000000..472a120
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/localdb.inc
@@ -0,0 +1,353 @@
+/*
+ gokz-localdb Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_localdb_included_
+#endinput
+#endif
+#define _gokz_localdb_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum DatabaseType
+{
+ DatabaseType_None = -1,
+ DatabaseType_MySQL,
+ DatabaseType_SQLite
+};
+
+enum
+{
+ JumpstatDB_Lookup_JumpID = 0,
+ JumpstatDB_Lookup_Distance,
+ JumpstatDB_Lookup_Block
+};
+
+enum
+{
+ JumpstatDB_FindPlayer_SteamID32 = 0,
+ JumpstatDB_FindPlayer_Alias
+};
+
+enum
+{
+ JumpstatDB_Top20_JumpID = 0,
+ JumpstatDB_Top20_SteamID,
+ JumpstatDB_Top20_Alias,
+ JumpstatDB_Top20_Block,
+ JumpstatDB_Top20_Distance,
+ JumpstatDB_Top20_Strafes,
+ JumpstatDB_Top20_Sync,
+ JumpstatDB_Top20_Pre,
+ JumpstatDB_Top20_Max,
+ JumpstatDB_Top20_Air
+};
+
+enum
+{
+ JumpstatDB_PBMenu_JumpID = 0,
+ JumpstatDB_PBMenu_JumpType,
+ JumpstatDB_PBMenu_Distance,
+ JumpstatDB_PBMenu_Strafes,
+ JumpstatDB_PBMenu_Sync,
+ JumpstatDB_PBMenu_Pre,
+ JumpstatDB_PBMenu_Max,
+ JumpstatDB_PBMenu_Air
+};
+
+enum
+{
+ JumpstatDB_BlockPBMenu_JumpID = 0,
+ JumpstatDB_BlockPBMenu_JumpType,
+ JumpstatDB_BlockPBMenu_Block,
+ JumpstatDB_BlockPBMenu_Distance,
+ JumpstatDB_BlockPBMenu_Strafes,
+ JumpstatDB_BlockPBMenu_Sync,
+ JumpstatDB_BlockPBMenu_Pre,
+ JumpstatDB_BlockPBMenu_Max,
+ JumpstatDB_BlockPBMenu_Air
+};
+
+enum
+{
+ JumpstatDB_Cache_Distance = 0,
+ JumpstatDB_Cache_Block,
+ JumpstatDB_Cache_BlockDistance,
+ JUMPSTATDB_CACHE_COUNT
+};
+
+enum
+{
+ TimerSetupDB_GetVBPos_SteamID = 0,
+ TimerSetupDB_GetVBPos_MapID,
+ TimerSetupDB_GetVBPos_Course,
+ TimerSetupDB_GetVBPos_IsStart,
+ TimerSetupDB_GetVBPos_PositionX,
+ TimerSetupDB_GetVBPos_PositionY,
+ TimerSetupDB_GetVBPos_PositionZ
+};
+
+enum
+{
+ TimerSetupDB_GetStartPos_SteamID = 0,
+ TimerSetupDB_GetStartPos_MapID,
+ TimerSetupDB_GetStartPos_PositionX,
+ TimerSetupDB_GetStartPos_PositionY,
+ TimerSetupDB_GetStartPos_PositionZ,
+ TimerSetupDB_GetStartPos_Angle0,
+ TimerSetupDB_GetStartPos_Angle1
+};
+
+enum DBOption:
+{
+ DBOption_AutoLoadTimerSetup = 0,
+ DBOPTION_COUNT
+};
+
+enum
+{
+ DBOption_Disabled = 0,
+ DBOption_Enabled,
+ DBOPTIONBOOL_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define GOKZ_DB_JS_DISTANCE_PRECISION 10000
+#define GOKZ_DB_JS_SYNC_PRECISION 100
+#define GOKZ_DB_JS_PRE_PRECISION 100
+#define GOKZ_DB_JS_MAX_PRECISION 100
+#define GOKZ_DB_JS_AIRTIME_PRECISION 10000
+#define GOKZ_DB_JS_MAX_JUMPS_PER_PLAYER 100
+
+stock char gC_DBOptionNames[DBOPTION_COUNT][] =
+{
+ "GOKZ DB - Auto Load Setup"
+};
+
+stock char gC_DBOptionDescriptions[DBOPTION_COUNT][] =
+{
+ "Automatically load timer setup on map start - 0 = Disabled, 1 = Enabled"
+}
+
+stock int gI_DBOptionDefaultValues[DBOPTION_COUNT] =
+{
+ DBOption_Disabled
+};
+
+stock int gI_DBOptionCounts[DBOPTION_COUNT] =
+{
+ DBOPTIONBOOL_COUNT
+};
+
+stock char gC_DBOptionPhrases[DBOPTION_COUNT][] =
+{
+ "Options Menu - Auto Load Timer Setup"
+};
+
+
+
+// =====[ TYPES ]=====
+
+typeset GetVBPositionCallback
+{
+ function Action(int client, const float position[3], int course, bool isStart);
+};
+
+typeset GetStartPositionCallback
+{
+ function Action(int client, const float position[3], const float angles[3]);
+};
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when gokz-localdb has connected to the database.
+ * Use GOKZ_DB_GetDatabase to get a clone of the database handle.
+ *
+ * @param DBType Database type.
+ */
+forward void GOKZ_DB_OnDatabaseConnect(DatabaseType DBType);
+
+/**
+ * Called when a player is ready for database interaction.
+ * At this point, the player is present and updated in the "Players" table.
+ *
+ * @param client Client index.
+ * @param steamID SteamID32 of the player (from GetSteamAccountID()).
+ * @param cheater Whether player is marked as a cheater in the database.
+ */
+forward void GOKZ_DB_OnClientSetup(int client, int steamID, bool cheater);
+
+/**
+ * Called when the current map is ready for database interaction.
+ * At this point, the map is present and updated in the "Maps" table.
+ *
+ * @param mapID MapID from the "Maps" table.
+ */
+forward void GOKZ_DB_OnMapSetup(int mapID);
+
+/**
+ * Called when a time has been inserted into the database.
+ *
+ * @param client Client index.
+ * @param steamID SteamID32 of the player (from GetSteamAccountID()).
+ * @param mapID MapID from the "Maps" table.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode Player's movement mode.
+ * @param style Player's movement style.
+ * @param runTimeMS Player's end time in milliseconds.
+ * @param teleportsUsed Number of teleports used by player.
+ */
+forward void GOKZ_DB_OnTimeInserted(
+ int client,
+ int steamID,
+ int mapID,
+ int course,
+ int mode,
+ int style,
+ int runTimeMS,
+ int teleportsUsed);
+
+/**
+ * Called when jumpstat PB has been achieved.
+ *
+ * @param client Client index.
+ * @param jumptype Type of the jump.
+ * @param mode Mode the jump was performed in.
+ * @param distance Distance jumped.
+ * @param block The size of the block jumped across.
+ * @param strafes The amount of strafes used.
+ * @param sync Keyboard/mouse synchronisation of the jump.
+ * @param pre Speed at takeoff.
+ * @param max Maximum speed during the jump.
+ * @param airtime Amount of time spend airborne.
+ */
+forward void GOKZ_DB_OnJumpstatPB(
+ int client,
+ int jumptype,
+ int mode,
+ float distance,
+ int block,
+ int strafes,
+ float sync,
+ float pre,
+ float max,
+ int airtime);
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets a clone of the GOKZ local database handle.
+ *
+ * @param database Database handle, or null if connection hasn't been made.
+ */
+native Database GOKZ_DB_GetDatabase();
+
+/**
+ * Gets the GOKZ local database type.
+ *
+ * @return Database type.
+ */
+native DatabaseType GOKZ_DB_GetDatabaseType();
+
+/**
+ * Gets whether client has been set up for GOKZ Local DB.
+ *
+ * @param client Client index.
+ * @return Whether GOKZ Local DB has set up the client.
+ */
+native bool GOKZ_DB_IsClientSetUp(int client);
+
+/**
+ * Gets whether GOKZ Local DB is set up for the current map.
+ *
+ * @return Whether GOKZ Local DB has set up the current map.
+ */
+native bool GOKZ_DB_IsMapSetUp();
+
+/**
+ * Gets the current map's MapID as in the "Maps" table.
+ *
+ * @return MapID from the "Maps" table.
+ */
+native int GOKZ_DB_GetCurrentMapID();
+
+/**
+ * Gets whether player is marked as a cheater in the database.
+ *
+ * @param client Client index.
+ * @return Whether player is marked as a cheater in the database.
+ */
+native bool GOKZ_DB_IsCheater(int client);
+
+/**
+ * Sets wheter player is marked as a cheater in the database.
+ *
+ * @param client Client index.
+ * @param cheater Whether to mark the player as a cheater.
+ */
+native void GOKZ_DB_SetCheater(int client, bool cheater);
+
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Converts a time float (seconds) to an integer (milliseconds).
+ *
+ * @param time Time in seconds.
+ * @return Time in milliseconds.
+ */
+stock int GOKZ_DB_TimeFloatToInt(float time)
+{
+ return RoundFloat(time * 1000.0);
+}
+
+/**
+ * Converts a time integer (milliseconds) to a float (seconds).
+ *
+ * @param time Time in milliseconds.
+ * @return Time in seconds.
+ */
+stock float GOKZ_DB_TimeIntToFloat(int time)
+{
+ return time / 1000.0;
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_localdb =
+{
+ name = "gokz-localdb",
+ file = "gokz-localdb.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_localdb_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_DB_GetDatabase");
+ MarkNativeAsOptional("GOKZ_DB_GetDatabaseType");
+ MarkNativeAsOptional("GOKZ_DB_IsClientSetUp");
+ MarkNativeAsOptional("GOKZ_DB_IsMapSetUp");
+ MarkNativeAsOptional("GOKZ_DB_GetCurrentMapID");
+ MarkNativeAsOptional("GOKZ_DB_IsCheater");
+ MarkNativeAsOptional("GOKZ_DB_SetCheater");
+}
+#endif
diff --git a/sourcemod/scripting/include/gokz/localranks.inc b/sourcemod/scripting/include/gokz/localranks.inc
new file mode 100644
index 0000000..914c6cb
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/localranks.inc
@@ -0,0 +1,176 @@
+/*
+ gokz-localranks Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_localranks_included_
+#endinput
+#endif
+#define _gokz_localranks_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ RecordType_Nub = 0,
+ RecordType_Pro,
+ RecordType_NubAndPro,
+ RECORDTYPE_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define LR_CFG_MAP_POOL "cfg/sourcemod/gokz/gokz-localranks-mappool.cfg"
+#define LR_CFG_SOUNDS "cfg/sourcemod/gokz/gokz-localranks-sounds.cfg"
+#define LR_COMMAND_COOLDOWN 2.5
+#define LR_MAP_TOP_CUTOFF 20
+#define LR_PLAYER_TOP_CUTOFF 20
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a player's time has been processed by GOKZ Local Ranks.
+ *
+ * @param client Client index.
+ * @param steamID SteamID32 of the player (from GetSteamAccountID()).
+ * @param mapID MapID from the "Maps" database table.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode Player's movement mode.
+ * @param style Player's movement style.
+ * @param runTime Player's end time.
+ * @param teleportsUsed Number of teleportsUsed used by player.
+ * @param firstTime Whether this is player's first time on this course.
+ * @param pbDiff Difference between new time and PB in seconds (-'ve means beat PB).
+ * @param rank New rank of the player's PB time.
+ * @param maxRank New total number of players with times.
+ * @param firstTimePro Whether this is player's first PRO time on this course.
+ * @param pbDiffPro Difference between new time and PRO PB in seconds (-'ve means beat PB).
+ * @param rankPro New rank of the player's PB PRO time.
+ * @param maxRankPro New total number of players with PRO times.
+ */
+forward void GOKZ_LR_OnTimeProcessed(
+ int client,
+ int steamID,
+ int mapID,
+ int course,
+ int mode,
+ int style,
+ float runTime,
+ int teleportsUsed,
+ bool firstTime,
+ float pbDiff,
+ int rank,
+ int maxRank,
+ bool firstTimePro,
+ float pbDiffPro,
+ int rankPro,
+ int maxRankPro);
+
+/**
+ * Called when a player sets a new local record.
+ *
+ * @param client Client index.
+ * @param steamID SteamID32 of the player (from GetSteamAccountID()).
+ * @param mapID MapID from the "Maps" table.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode Player's movement mode.
+ * @param style Player's movement style.
+ * @param recordType Type of record.
+ */
+forward void GOKZ_LR_OnNewRecord(
+ int client,
+ int steamID,
+ int mapID,
+ int course,
+ int mode,
+ int style,
+ int recordType,
+ float pbDiff,
+ int teleportsUsed);
+
+/**
+ * Called when a player misses the server record time.
+ * Is called regardless of player's current run type.
+ *
+ * @param client Client index.
+ * @param recordTime Record time missed.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode Player's movement mode.
+ * @param style Player's movement style.
+ * @param recordType Type of record.
+ */
+forward void GOKZ_LR_OnRecordMissed(int client, float recordTime, int course, int mode, int style, int recordType);
+
+/**
+ * Called when a player misses their personal best time.
+ * Is called regardless of player's current run type.
+ *
+ * @param client Client index.
+ * @param pbTime Personal best time missed.
+ * @param course Course number e.g. 0=main, 1='bonus1' etc.
+ * @param mode Player's movement mode.
+ * @param style Player's movement style.
+ * @param recordType Type of record.
+ */
+forward void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType);
+
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets whether player has missed the server record time.
+ *
+ * @param client Client index.
+ * @param timeType Which record time i.e. NUB or PRO.
+ * @return Whether player has missed the server record time.
+ */
+native bool GOKZ_LR_GetRecordMissed(int client, int timeType);
+
+/**
+ * Gets whether player has missed their personal best time.
+ *
+ * @param client Client index.
+ * @param timeType Which PB time i.e. NUB or PRO.
+ * @return Whether player has missed their PB time.
+ */
+native bool GOKZ_LR_GetPBMissed(int client, int timeType);
+
+/**
+ * Reopens the map top menu with the already selected parameters.
+ * Don't use if the client hasn't opened the map top menu before.
+ *
+ * @param client Client index.
+ */
+native void GOKZ_LR_ReopenMapTopMenu(int client);
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_localranks =
+{
+ name = "gokz-localranks",
+ file = "gokz-localranks.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_localranks_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_LR_GetRecordMissed");
+ MarkNativeAsOptional("GOKZ_LR_GetPBMissed");
+ MarkNativeAsOptional("GOKZ_LR_ReopenMapTopMenu");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/momsurffix.inc b/sourcemod/scripting/include/gokz/momsurffix.inc
new file mode 100644
index 0000000..65f603e
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/momsurffix.inc
@@ -0,0 +1,23 @@
+/*
+ gokz-momsurffix Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_momsurffix_included_
+#endinput
+#endif
+#define _gokz_momsurffix_included_
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_momsurffix =
+{
+ name = "gokz-momsurffix",
+ file = "gokz-momsurffix.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/paint.inc b/sourcemod/scripting/include/gokz/paint.inc
new file mode 100644
index 0000000..19f4fb5
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/paint.inc
@@ -0,0 +1,114 @@
+/*
+ gokz-paint Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_paint_included_
+#endinput
+#endif
+#define _gokz_paint_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum PaintOption:
+{
+ PAINTOPTION_INVALID = -1,
+ PaintOption_Color,
+ PaintOption_Size,
+ PAINTOPTION_COUNT
+};
+
+enum
+{
+ PaintColor_Red = 0,
+ PaintColor_White,
+ PaintColor_Black,
+ PaintColor_Blue,
+ PaintColor_Brown,
+ PaintColor_Green,
+ PaintColor_Yellow,
+ PaintColor_Purple,
+ PAINTCOLOR_COUNT
+};
+
+enum
+{
+ PaintSize_Small = 0,
+ PaintSize_Medium,
+ PaintSize_Big,
+ PAINTSIZE_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define PAINT_OPTION_CATEGORY "Paint"
+#define MIN_PAINT_SPACING 1.0
+
+stock char gC_PaintOptionNames[PAINTOPTION_COUNT][] =
+{
+ "GOKZ Paint - Color",
+ "GOKZ Paint - Size"
+};
+
+stock char gC_PaintOptionDescriptions[PAINTOPTION_COUNT][] =
+{
+ "Paint Color",
+ "Paint Size - 0 = Small, 1 = Medium, 2 = Big"
+};
+
+stock char gC_PaintOptionPhrases[PAINTOPTION_COUNT][] =
+{
+ "Options Menu - Paint Color",
+ "Options Menu - Paint Size"
+};
+
+stock int gI_PaintOptionCounts[PAINTOPTION_COUNT] =
+{
+ PAINTCOLOR_COUNT,
+ PAINTSIZE_COUNT
+};
+
+stock int gI_PaintOptionDefaults[PAINTOPTION_COUNT] =
+{
+ PaintColor_Red,
+ PaintSize_Medium
+};
+
+stock char gC_PaintColorPhrases[PAINTCOLOR_COUNT][] =
+{
+ "Options Menu - Red",
+ "Options Menu - White",
+ "Options Menu - Black",
+ "Options Menu - Blue",
+ "Options Menu - Brown",
+ "Options Menu - Green",
+ "Options Menu - Yellow",
+ "Options Menu - Purple"
+};
+
+stock char gC_PaintSizePhrases[PAINTSIZE_COUNT][] =
+{
+ "Options Menu - Small",
+ "Options Menu - Medium",
+ "Options Menu - Big"
+};
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_paint =
+{
+ name = "gokz-paint",
+ file = "gokz-paint.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
diff --git a/sourcemod/scripting/include/gokz/pistol.inc b/sourcemod/scripting/include/gokz/pistol.inc
new file mode 100644
index 0000000..1edd5f9
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/pistol.inc
@@ -0,0 +1,93 @@
+/*
+ gokz-pistol Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_pistol_included_
+#endinput
+#endif
+#define _gokz_pistol_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ Pistol_Disabled = 0,
+ Pistol_USPS,
+ Pistol_Glock18,
+ Pistol_DualBerettas,
+ Pistol_P250,
+ Pistol_FiveSeveN,
+ Pistol_Tec9,
+ Pistol_CZ75Auto,
+ Pistol_DesertEagle,
+ Pistol_R8Revolver,
+ PISTOL_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define PISTOL_OPTION_NAME "GOKZ - Pistol"
+#define PISTOL_OPTION_DESCRIPTION "Pistol - 0 = Disabled, 1 = USP-S / P2000, 2 = Glock-18, 3 = Dual Berettas, 4 = P250, 5 = Five-SeveN, 6 = Tec-9, 7 = CZ75-Auto, 8 = Desert Eagle, 9 = R8 Revolver"
+
+stock char gC_PistolNames[PISTOL_COUNT][] =
+{
+ "", // Disabled
+ "USP-S / P2000",
+ "Glock-18",
+ "Dual Berettas",
+ "P250",
+ "Five-SeveN",
+ "Tec-9",
+ "CZ75-Auto",
+ "Desert Eagle",
+ "R8 Revolver"
+};
+
+stock char gC_PistolClassNames[PISTOL_COUNT][] =
+{
+ "", // Disabled
+ "weapon_hkp2000",
+ "weapon_glock",
+ "weapon_elite",
+ "weapon_p250",
+ "weapon_fiveseven",
+ "weapon_tec9",
+ "weapon_cz75a",
+ "weapon_deagle",
+ "weapon_revolver"
+};
+
+stock int gI_PistolTeams[PISTOL_COUNT] =
+{
+ CS_TEAM_NONE, // Disabled
+ CS_TEAM_CT,
+ CS_TEAM_T,
+ CS_TEAM_NONE,
+ CS_TEAM_NONE,
+ CS_TEAM_CT,
+ CS_TEAM_T,
+ CS_TEAM_NONE,
+ CS_TEAM_NONE,
+ CS_TEAM_NONE
+};
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_pistol =
+{
+ name = "gokz-pistol",
+ file = "gokz-pistol.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/profile.inc b/sourcemod/scripting/include/gokz/profile.inc
new file mode 100644
index 0000000..70d314a
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/profile.inc
@@ -0,0 +1,291 @@
+/*
+ gokz-profile Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_profile_included_
+#endinput
+#endif
+#define _gokz_profile_included_
+
+
+// =====[ RANKS ]=====
+
+#define RANK_COUNT 23
+
+stock int gI_rankThreshold[MODE_COUNT][RANK_COUNT] = {
+ {
+ 0,
+ 1,
+ 500,
+ 1000,
+
+ 2000,
+ 5000,
+ 10000,
+
+ 20000,
+ 30000,
+ 40000,
+
+ 60000,
+ 70000,
+ 80000,
+
+ 100000,
+ 120000,
+ 140000,
+
+ 160000,
+ 180000,
+ 200000,
+
+ 250000,
+ 300000,
+ 400000,
+ 600000
+ },
+ {
+ 0,
+ 1,
+ 500,
+ 1000,
+
+ 2000,
+ 5000,
+ 10000,
+
+ 20000,
+ 30000,
+ 40000,
+
+ 60000,
+ 70000,
+ 80000,
+
+ 100000,
+ 120000,
+ 150000,
+
+ 200000,
+ 230000,
+ 250000,
+
+ 300000,
+ 400000,
+ 500000,
+ 800000
+ },
+ {
+ 0,
+ 1,
+ 500,
+ 1000,
+
+ 2000,
+ 5000,
+ 10000,
+
+ 20000,
+ 30000,
+ 40000,
+
+ 60000,
+ 70000,
+ 80000,
+
+ 100000,
+ 120000,
+ 150000,
+
+ 200000,
+ 230000,
+ 250000,
+
+ 400000,
+ 600000,
+ 800000,
+ 1000000
+ },
+};
+
+stock char gC_rankName[RANK_COUNT][] = {
+ "New",
+ "Beginner-",
+ "Beginner",
+ "Beginner+",
+ "Amateur-",
+ "Amateur",
+ "Amateur+",
+ "Casual-",
+ "Casual",
+ "Casual+",
+ "Regular-",
+ "Regular",
+ "Regular+",
+ "Skilled-",
+ "Skilled",
+ "Skilled+",
+ "Expert-",
+ "Expert",
+ "Expert+",
+ "Semipro",
+ "Pro",
+ "Master",
+ "Legend"
+};
+
+stock char gC_rankColor[RANK_COUNT][] = {
+ "{grey}",
+ "{default}",
+ "{default}",
+ "{default}",
+ "{blue}",
+ "{blue}",
+ "{blue}",
+ "{lightgreen}",
+ "{lightgreen}",
+ "{lightgreen}",
+ "{green}",
+ "{green}",
+ "{green}",
+ "{purple}",
+ "{purple}",
+ "{purple}",
+ "{orchid}",
+ "{orchid}",
+ "{orchid}",
+ "{lightred}",
+ "{lightred}",
+ "{red}",
+ "{gold}"
+};
+
+
+// =====[ ENUMS ]=====
+
+enum ProfileOption:
+{
+ PROFILEOPTION_INVALID = -1,
+ ProfileOption_ShowRankChat,
+ ProfileOption_ShowRankClanTag,
+ ProfileOption_TagType,
+ PROFILEOPTION_COUNT
+};
+
+enum
+{
+ ProfileOptionBool_Disabled = 0,
+ ProfileOptionBool_Enabled,
+ PROFILEOPTIONBOOL_COUNT
+};
+
+enum
+{
+ ProfileTagType_Rank = 0,
+ ProfileTagType_Admin,
+ ProfileTagType_VIP,
+ PROFILETAGTYPE_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+stock char gC_ProfileOptionNames[PROFILEOPTION_COUNT][] =
+{
+ "GOKZ Profile - Show Rank Chat",
+ "GOKZ Profile - Show Rank Clan",
+ "GOKZ Profile - Tag Type"
+};
+
+stock char gC_ProfileOptionDescriptions[PROFILEOPTION_COUNT][] =
+{
+ "Show Rank Tag in Chat - 0 = Disabled, 1 = Enabled",
+ "Show Rank in Clan - 0 = Disabled, 1 = Enabled",
+ "Type of Tag to Show - 0 = Rank, 1 = Admin, 2 = VIP"
+};
+
+stock char gC_ProfileOptionPhrases[PROFILEOPTION_COUNT][] =
+{
+ "Options Menu - Show Rank Chat",
+ "Options Menu - Show Rank Clan",
+ "Options Menu - Tag Type",
+};
+
+stock char gC_ProfileBoolPhrases[PROFILEOPTIONBOOL_COUNT][] =
+{
+ "Options Menu - Disabled",
+ "Options Menu - Enabled"
+};
+
+stock char gC_ProfileTagTypePhrases[PROFILETAGTYPE_COUNT][] =
+{
+ "Options Menu - Tag Rank",
+ "Options Menu - Tag Admin",
+ "Options Menu - Tag VIP"
+};
+
+stock int gI_ProfileOptionCounts[PROFILEOPTION_COUNT] =
+{
+ PROFILEOPTIONBOOL_COUNT,
+ PROFILEOPTIONBOOL_COUNT,
+ PROFILETAGTYPE_COUNT
+};
+
+stock int gI_ProfileOptionDefaults[PROFILEOPTION_COUNT] =
+{
+ ProfileOptionBool_Enabled,
+ ProfileOptionBool_Enabled,
+ ProfileTagType_Rank
+};
+
+#define PROFILE_OPTION_CATEGORY "Profile"
+#define TAG_COLOR_ADMIN "{red}"
+#define TAG_COLOR_VIP "{purple}"
+
+
+// =====[ FORWARDS ]=====
+
+
+/**
+ * Called when the rank of a player is updated.
+ *
+ * @param client Client index.
+ * @param mode Game mode.
+ * @param rank The new rank.
+ */
+forward void GOKZ_PF_OnRankUpdated(int client, int mode, int rank);
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets whether a mode is loaded.
+ *
+ * @param client Client.
+ * @param tag Mode.
+ * @returns Integer representing the player rank.
+ */
+native int GOKZ_PF_GetRank(int client, int mode);
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_profile =
+{
+ name = "gokz-profile",
+ file = "gokz-profile.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_profile_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_PF_GetRank");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/quiet.inc b/sourcemod/scripting/include/gokz/quiet.inc
new file mode 100644
index 0000000..a328b7e
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/quiet.inc
@@ -0,0 +1,205 @@
+/*
+ gokz-quiet Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_quiet_included_
+#endinput
+#endif
+#define _gokz_quiet_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum QTOption:
+{
+ QTOPTION_INVALID = -1,
+ QTOption_ShowPlayers,
+ QTOption_Soundscapes,
+ QTOption_FallDamageSound,
+ QTOption_AmbientSounds,
+ QTOption_CheckpointVolume,
+ QTOption_TeleportVolume,
+ QTOption_TimerVolume,
+ QTOption_ErrorVolume,
+ QTOption_ServerRecordVolume,
+ QTOption_WorldRecordVolume,
+ QTOption_JumpstatsVolume,
+ QTOPTION_COUNT
+};
+
+enum
+{
+ ShowPlayers_Disabled = 0,
+ ShowPlayers_Enabled,
+ SHOWPLAYERS_COUNT
+};
+
+enum
+{
+ Soundscapes_Disabled = 0,
+ Soundscapes_Enabled,
+ SOUNDSCAPES_COUNT
+};
+
+// =====[ CONSTANTS ]=====
+
+#define QUIET_OPTION_CATEGORY "Quiet"
+#define DEFAULT_VOLUME 10
+#define VOLUME_COUNT 21 // Maximum of 200%
+
+#define EFFECT_IMPACT 8
+#define EFFECT_KNIFESLASH 2
+#define BLANK_SOUNDSCAPEINDEX 482 // Search for "coopcementplant.missionselect_blank" id with sv_soundscape_printdebuginfo.
+
+stock char gC_QTOptionNames[QTOPTION_COUNT][] =
+{
+ "GOKZ QT - Show Players",
+ "GOKZ QT - Soundscapes",
+ "GOKZ QT - Fall Damage Sound",
+ "GOKZ QT - Ambient Sounds",
+ "GOKZ QT - Checkpoint Volume",
+ "GOKZ QT - Teleport Volume",
+ "GOKZ QT - Timer Volume",
+ "GOKZ QT - Error Volume",
+ "GOKZ QT - Server Record Volume",
+ "GOKZ QT - World Record Volume",
+ "GOKZ QT - Jumpstats Volume"
+};
+
+stock char gC_QTOptionDescriptions[QTOPTION_COUNT][] =
+{
+ "Visibility of Other Players - 0 = Disabled, 1 = Enabled",
+ "Play Soundscapes - 0 = Disabled, 1 = Enabled",
+ "Play Fall Damage Sound - 0 to 20 = 0% to 200%",
+ "Play Ambient Sounds - 0 to 20 = 0% to 200%",
+ "Checkpoint Volume - 0 to 20 = 0% to 200%",
+ "Teleport Volume - 0 to 20 = 0% to 200%",
+ "Timer Volume - 0 to 20 = 0% to 200%",
+ "Error Volume - 0 to 20 = 0% to 200%",
+ "Server Record Volume - 0 to 20 = 0% to 200%",
+ "World Record Volume - 0 to 20 = 0% to 200%",
+ "Jumpstats Volume - 0 to 20 = 0% to 200%"
+};
+
+stock int gI_QTOptionDefaultValues[QTOPTION_COUNT] =
+{
+ ShowPlayers_Enabled,
+ Soundscapes_Enabled,
+ DEFAULT_VOLUME, // Fall damage volume
+ DEFAULT_VOLUME, // Ambient volume
+ DEFAULT_VOLUME, // Checkpoint volume
+ DEFAULT_VOLUME, // Teleport volume
+ DEFAULT_VOLUME, // Timer volume
+ DEFAULT_VOLUME, // Error volume
+ DEFAULT_VOLUME, // Server Record Volume
+ DEFAULT_VOLUME, // World Record Volume
+ DEFAULT_VOLUME // Jumpstats Volume
+};
+
+stock int gI_QTOptionCounts[QTOPTION_COUNT] =
+{
+ SHOWPLAYERS_COUNT,
+ SOUNDSCAPES_COUNT,
+ VOLUME_COUNT, // Fall damage volume
+ VOLUME_COUNT, // Ambient volume
+ VOLUME_COUNT, // Checkpoint volume
+ VOLUME_COUNT, // Teleport volume
+ VOLUME_COUNT, // Timer volume
+ VOLUME_COUNT, // Error volume
+ VOLUME_COUNT, // Server Record volume
+ VOLUME_COUNT, // World Record volume
+ VOLUME_COUNT // Jumpstats volume
+};
+
+stock char gC_QTOptionPhrases[QTOPTION_COUNT][] =
+{
+ "Options Menu - Show Players",
+ "Options Menu - Soundscapes",
+ "Options Menu - Fall Damage Sounds",
+ "Options Menu - Ambient Sounds",
+ "Options Menu - Checkpoint Volume",
+ "Options Menu - Teleport Volume",
+ "Options Menu - Timer Volume",
+ "Options Menu - Error Volume",
+ "Options Menu - Server Record Volume",
+ "Options Menu - World Record Volume",
+ "Options Menu - Jumpstats Volume"
+};
+
+// =====[ STOCKS ]=====
+
+/**
+ * Returns whether an option is a gokz-quiet option.
+ *
+ * @param option Option name.
+ * @param optionEnum Variable to store enumerated gokz-quiet option (if it is one).
+ * @return Whether option is a gokz-quiet option.
+ */
+stock bool GOKZ_QT_IsQTOption(const char[] option, QTOption &optionEnum = QTOPTION_INVALID)
+{
+ for (QTOption i; i < QTOPTION_COUNT; i++)
+ {
+ if (StrEqual(option, gC_QTOptionNames[i]))
+ {
+ optionEnum = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Gets the current value of a player's gokz-quiet option.
+ *
+ * @param client Client index.
+ * @param option gokz-quiet option.
+ * @return Current value of option.
+ */
+stock any GOKZ_QT_GetOption(int client, QTOption option)
+{
+ return GOKZ_GetOption(client, gC_QTOptionNames[option]);
+}
+
+/**
+ * Sets a player's gokz-quiet option's value.
+ *
+ * @param client Client index.
+ * @param option gokz-quiet option.
+ * @param value New option value.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_QT_SetOption(int client, QTOption option, any value)
+{
+ return GOKZ_SetOption(client, gC_QTOptionNames[option], value);
+}
+
+/**
+ * Increment an integer-type gokz-quiet option's value.
+ * Loops back to '0' if max value is exceeded.
+ *
+ * @param client Client index.
+ * @param option gokz-quiet option.
+ * @return Whether option was successfully set.
+ */
+stock bool GOKZ_QT_CycleOption(int client, QTOption option)
+{
+ return GOKZ_CycleOption(client, gC_QTOptionNames[option]);
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_quiet =
+{
+ name = "gokz-quiet",
+ file = "gokz-quiet.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/racing.inc b/sourcemod/scripting/include/gokz/racing.inc
new file mode 100644
index 0000000..d6819ea
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/racing.inc
@@ -0,0 +1,189 @@
+/*
+ gokz-racing Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_racing_included_
+#endinput
+#endif
+#define _gokz_racing_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum RaceInfo:
+{
+ RaceInfo_ID,
+ RaceInfo_Status,
+ RaceInfo_HostUserID,
+ RaceInfo_FinishedRacerCount,
+ RaceInfo_Type,
+ RaceInfo_Course,
+ RaceInfo_Mode,
+ RaceInfo_CheckpointRule,
+ RaceInfo_CooldownRule,
+ RACEINFO_COUNT
+};
+
+enum
+{
+ RaceType_Normal,
+ RaceType_Duel,
+ RACETYPE_COUNT
+};
+
+enum
+{
+ RaceStatus_Pending,
+ RaceStatus_Countdown,
+ RaceStatus_Started,
+ RaceStatus_Aborting,
+ RACESTATUS_COUNT
+};
+
+enum
+{
+ RacerStatus_Available,
+ RacerStatus_Pending,
+ RacerStatus_Accepted,
+ RacerStatus_Racing,
+ RacerStatus_Finished,
+ RacerStatus_Surrendered,
+ RACERSTATUS_COUNT
+};
+
+enum
+{
+ CheckpointRule_None,
+ CheckpointRule_Limit,
+ CheckpointRule_Cooldown,
+ CheckpointRule_Unlimited,
+ CHECKPOINTRULE_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define RC_COUNTDOWN_TIME 10.0
+#define RC_REQUEST_TIMEOUT_TIME 15.0
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a player has finished their race.
+ *
+ * @param client Client index.
+ * @param raceID ID of the race.
+ * @param place Final placement in the race.
+ */
+forward void GOKZ_RC_OnFinish(int client, int raceID, int place);
+
+/**
+ * Called when a player has surrendered their race.
+ *
+ * @param client Client index.
+ * @param raceID ID of the race.
+ */
+forward void GOKZ_RC_OnSurrender(int client, int raceID);
+
+/**
+ * Called when a player receives a race request.
+ *
+ * @param client Client index.
+ * @param raceID ID of the race.
+ */
+forward void GOKZ_RC_OnRequestReceived(int client, int raceID)
+
+/**
+ * Called when a player accepts a race request.
+ *
+ * @param client Client index.
+ * @param raceID ID of the race.
+ */
+forward void GOKZ_RC_OnRequestAccepted(int client, int raceID)
+
+/**
+ * Called when a player declines a race request.
+ *
+ * @param client Client index.
+ * @param raceID ID of the race.
+ * @param timeout Whether the client was too late to respond.
+ */
+forward void GOKZ_RC_OnRequestDeclined(int client, int raceID, bool timeout)
+
+/**
+ * Called when a race has been registered.
+ * The initial status of a race is RaceStatus_Pending.
+ *
+ * @param raceID ID of the race.
+ */
+forward void GOKZ_RC_OnRaceRegistered(int raceID);
+
+/**
+ * Called when a race's info property has changed.
+ *
+ * @param raceID ID of the race.
+ * @param prop Info property that was changed.
+ * @param oldValue Previous value.
+ * @param newValue New value.
+ */
+forward void GOKZ_RC_OnRaceInfoChanged(int raceID, RaceInfo prop, int oldValue, int newValue);
+
+
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets the value of a race info property.
+ *
+ * @param raceID Race index.
+ * @param prop Info property to get.
+ * @return Value of the info property.
+ */
+native int GOKZ_RC_GetRaceInfo(int raceID, RaceInfo prop);
+
+/**
+ * Gets a player's racer status.
+ * Refer to the RacerStatus enumeration.
+ *
+ * @param client Client index.
+ * @return Racer status of the client.
+ */
+native int GOKZ_RC_GetStatus(int client);
+
+/**
+ * Gets the ID of the race a player is in.
+ *
+ * @param client Client index.
+ * @return ID of the race the player is in, or -1 if not in a race.
+ */
+native int GOKZ_RC_GetRaceID(int client);
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_racing =
+{
+ name = "gokz-racing",
+ file = "gokz-racing.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_racing_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_RC_GetRaceInfo");
+ MarkNativeAsOptional("GOKZ_RC_GetStatus");
+ MarkNativeAsOptional("GOKZ_RC_GetRaceID");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/replays.inc b/sourcemod/scripting/include/gokz/replays.inc
new file mode 100644
index 0000000..6aabdbd
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/replays.inc
@@ -0,0 +1,275 @@
+/*
+ gokz-replays Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_replays_included_
+#endinput
+#endif
+#define _gokz_replays_included_
+
+// Bit of a hack, but need it for other plugins that depend on replays to compile
+#if defined REQUIRE_PLUGIN
+#undef REQUIRE_PLUGIN
+#include <gokz/anticheat>
+#define REQUIRE_PLUGIN
+#else
+#include <gokz/anticheat>
+#endif
+
+
+
+// =====[ ENUMS ]=====
+enum
+{
+ ReplayType_Run = 0,
+ ReplayType_Cheater,
+ ReplayType_Jump,
+ REPLAYTYPE_COUNT
+};
+
+enum ReplaySaveState
+{
+ ReplaySave_Local = 0,
+ ReplaySave_Temp,
+ ReplaySave_Disabled
+};
+
+// NOTE: Replays use delta compression for storage.
+// This enum is the indices of the ReplayTickData enum struct.
+// NOTE: This has to match the ReplayTickData enum struct!!!
+enum
+{
+ RPDELTA_DELTAFLAGS = 0,
+ RPDELTA_DELTAFLAGS2,
+ RPDELTA_VEL_X,
+ RPDELTA_VEL_Y,
+ RPDELTA_VEL_Z,
+ RPDELTA_MOUSE_X,
+ RPDELTA_MOUSE_Y,
+ RPDELTA_ORIGIN_X,
+ RPDELTA_ORIGIN_Y,
+ RPDELTA_ORIGIN_Z,
+ RPDELTA_ANGLES_X,
+ RPDELTA_ANGLES_Y,
+ RPDELTA_ANGLES_Z,
+ RPDELTA_VELOCITY_X,
+ RPDELTA_VELOCITY_Y,
+ RPDELTA_VELOCITY_Z,
+ RPDELTA_FLAGS,
+ RPDELTA_PACKETSPERSECOND,
+ RPDELTA_LAGGEDMOVEMENTVALUE,
+ RPDELTA_BUTTONSFORCED,
+
+ RP_V2_TICK_DATA_BLOCKSIZE
+};
+
+
+
+// =====[ STRUCTS ] =====
+
+enum struct GeneralReplayHeader
+{
+ int magicNumber;
+ int formatVersion;
+ int replayType;
+ char gokzVersion[32];
+ char mapName[64];
+ int mapFileSize;
+ int serverIP;
+ int timestamp;
+ char playerAlias[MAX_NAME_LENGTH];
+ int playerSteamID;
+ int mode;
+ int style;
+ float playerSensitivity;
+ float playerMYaw;
+ float tickrate;
+ int tickCount;
+ int equippedWeapon;
+ int equippedKnife;
+}
+
+enum struct JumpReplayHeader
+{
+ int jumpType;
+ float distance;
+ int blockDistance;
+ int strafeCount;
+ float sync;
+ float pre;
+ float max;
+ int airtime;
+}
+
+enum struct CheaterReplayHeader
+{
+ ACReason ACReason;
+}
+
+enum struct RunReplayHeader
+{
+ float time;
+ int course;
+ int teleportsUsed;
+}
+
+// NOTE: Make sure to change the RPDELTA_* enum, TickDataToArray() and TickDataFromArray() when adding/removing stuff from this!!!
+enum struct ReplayTickData
+{
+ int deltaFlags;
+ int deltaFlags2;
+ float vel[3];
+ int mouse[2];
+ float origin[3];
+ float angles[3];
+ float velocity[3];
+ int flags;
+ float packetsPerSecond;
+ float laggedMovementValue;
+ int buttonsForced;
+}
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define RP_DIRECTORY "data/gokz-replays" // In Path_SM
+#define RP_DIRECTORY_RUNS "data/gokz-replays/_runs" // In Path_SM
+#define RP_DIRECTORY_RUNS_TEMP "data/gokz-replays/_tempRuns" // In Path_SM
+#define RP_DIRECTORY_CHEATERS "data/gokz-replays/_cheaters" // In Path_SM
+#define RP_DIRECTORY_JUMPS "data/gokz-replays/_jumps" // In Path_SM
+#define RP_DIRECTORY_BLOCKJUMPS "blocks"
+#define RP_FILE_EXTENSION "replay"
+#define RP_MAGIC_NUMBER 0x676F6B7A
+#define RP_FORMAT_VERSION 0x02
+#define RP_NAV_FILE "maps/gokz-replays.nav"
+#define RP_V1_TICK_DATA_BLOCKSIZE 7
+#define RP_CACHE_BLOCKSIZE 4
+#define RP_MAX_BOTS 4
+#define RP_PLAYBACK_BREATHER_TIME 2.0
+#define RP_MIN_CHEATER_REPLAY_LENGTH 30 // 30 seconds
+#define RP_MAX_CHEATER_REPLAY_LENGTH 120 // 2 minutes
+#define RP_MAX_BHOP_GROUND_TICKS 5
+#define RP_SKIP_TIME 10 // 10 seconds
+#define RP_MAX_DURATION 6451200 // 14 hours on 128 tick
+#define RP_JUMP_STEP_SOUND_THRESHOLD 140.0
+#define RP_PLAYER_ACCELSPEED 450.0
+
+#define RP_MOVETYPE_MASK (0xF)
+#define RP_IN_ATTACK (1 << 4)
+#define RP_IN_ATTACK2 (1 << 5)
+#define RP_IN_JUMP (1 << 6)
+#define RP_IN_DUCK (1 << 7)
+#define RP_IN_FORWARD (1 << 8)
+#define RP_IN_BACK (1 << 9)
+#define RP_IN_LEFT (1 << 10)
+#define RP_IN_RIGHT (1 << 11)
+#define RP_IN_MOVELEFT (1 << 12)
+#define RP_IN_MOVERIGHT (1 << 13)
+#define RP_IN_RELOAD (1 << 14)
+#define RP_IN_SPEED (1 << 15)
+#define RP_IN_USE (1 << 16)
+#define RP_IN_BULLRUSH (1 << 17)
+#define RP_FL_ONGROUND (1 << 18)
+#define RP_FL_DUCKING (1 << 19)
+#define RP_FL_SWIM (1 << 20)
+#define RP_UNDER_WATER (1 << 21)
+#define RP_TELEPORT_TICK (1 << 22)
+#define RP_TAKEOFF_TICK (1 << 23)
+#define RP_HIT_PERF (1 << 24)
+#define RP_SECONDARY_EQUIPPED (1 << 25)
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a replay of a player is written to disk.
+ * This includes replays of cheaters which are saved if
+ * the player is marked as a cheater by gokz-localdb.
+ *
+ * @param client The client ID of the player who completed the run.
+ * @param replayType The type of the replay (Run/Jump/Cheater).
+ * @param map The name of the map the run was completed on.
+ * @param course The specific course on the map the run was completed on.
+ * @param timeType The type of time (Pro/Nub).
+ * @param time The time the run was completed in.
+ * @param filePath Replay file path.
+ * @param tempReplay Whether the replay file should only be temporaily stored.
+ * @return Plugin_Handled to take over the temporary replay deletion, Plugin_Continue to allow temporary replay deletion by the replay plugin.
+ */
+forward Action GOKZ_RP_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay);
+
+/**
+ * Called when a currently being recorded replay is discarded from
+ * memory and recording has been stopped (without writing it to disk).
+ *
+ * @param client Client index.
+ */
+forward void GOKZ_RP_OnReplayDiscarded(int client);
+
+/**
+ * Called when a player has ended their timer, and gokz-replays has
+ * processed the time and has possibly written a replay to disk.
+ *
+ * @param client Client index.
+ * @param filePath Replay file path, or "" if no replay saved.
+ * @param course Course number.
+ * @param time Player's end time.
+ * @param teleportsUsed Number of teleports used by player.
+ */
+forward void GOKZ_RP_OnTimerEnd_Post(int client, const char[] filePath, int course, float time, int teleportsUsed);
+
+
+
+// =====[ NATIVES ]====
+
+/**
+ * Called by the HUD to get the state of the current replay.
+ *
+ * @param client Client index.
+ * @param info Struct to pass the values into.
+ * @return If successful
+ */
+native int GOKZ_RP_GetPlaybackInfo(int client, any[] info);
+
+/**
+ * Called by the LocalDB to initiate a replay of a jump
+ *
+ * @param client Client index.
+ * @param path Path to the replay file.
+ * @return The client ID of the bot performing the replay.
+ */
+native int GOKZ_RP_LoadJumpReplay(int client, char[] path);
+
+/**
+ * Called by the HUD to show the replay control menu.
+ *
+ * @param client Client index.
+ */
+native bool GOKZ_RP_UpdateReplayControlMenu(int client);
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_replays =
+{
+ name = "gokz-replays",
+ file = "gokz-replays.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_gokz_replays_SetNTVOptional()
+{
+ MarkNativeAsOptional("GOKZ_RP_GetPlaybackInfo");
+ MarkNativeAsOptional("GOKZ_RP_LoadJumpReplay");
+ MarkNativeAsOptional("GOKZ_RP_UpdateReplayControlMenu");
+}
+#endif
diff --git a/sourcemod/scripting/include/gokz/slayonend.inc b/sourcemod/scripting/include/gokz/slayonend.inc
new file mode 100644
index 0000000..2ed01e5
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/slayonend.inc
@@ -0,0 +1,43 @@
+/*
+ gokz-slayonend Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_slayonend_included_
+#endinput
+#endif
+#define _gokz_slayonend_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ SlayOnEnd_Disabled = 0,
+ SlayOnEnd_Enabled,
+ SLAYONEND_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define SLAYONEND_OPTION_NAME "GOKZ - Slay On End"
+#define SLAYONEND_OPTION_DESCRIPTION "Automatic Slaying Upon Ending Timer - 0 = Disabled, 1 = Enabled"
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_slayonend =
+{
+ name = "gokz-slayonend",
+ file = "gokz-slayonend.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/tips.inc b/sourcemod/scripting/include/gokz/tips.inc
new file mode 100644
index 0000000..b5e2d3e
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/tips.inc
@@ -0,0 +1,59 @@
+/*
+ gokz-tips Plugin Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+*/
+
+#if defined _gokz_tips_included_
+#endinput
+#endif
+#define _gokz_tips_included_
+
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ Tips_Disabled = 0,
+ Tips_Enabled,
+ TIPS_COUNT
+};
+
+
+
+// =====[ CONSTANTS ]=====
+
+#define TIPS_PLUGINS_COUNT 9
+#define TIPS_CORE "gokz-tips-core.phrases.txt"
+#define TIPS_TIPS "gokz-tips-tips.phrases.txt"
+#define TIPS_OPTION_NAME "GOKZ - Tips"
+#define TIPS_OPTION_DESCRIPTION "Random Tips Periodically in Chat - 0 = Disabled, 1 = Enabled"
+
+stock char gC_PluginsWithTips[TIPS_PLUGINS_COUNT][] =
+{
+ "goto",
+ "hud",
+ "jumpstats",
+ "localranks",
+ "measure",
+ "pistol",
+ "quiet",
+ "replays",
+ "spec"
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_tips =
+{
+ name = "gokz-tips",
+ file = "gokz-tips.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/tpanglefix.inc b/sourcemod/scripting/include/gokz/tpanglefix.inc
new file mode 100644
index 0000000..fc8faa2
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/tpanglefix.inc
@@ -0,0 +1,40 @@
+/*
+ gokz-tpanglefix Plugin Include
+
+ Website: https://github.com/KZGlobalTeam/gokz
+*/
+
+#if defined _gokz_tpanglefix_included_
+#endinput
+#endif
+#define _gokz_tpanglefix_included_
+
+
+// =====[ ENUMS ]=====
+
+enum
+{
+ TPAngleFix_Disabled = 0,
+ TPAngleFix_Enabled,
+ TPANGLEFIX_COUNT
+};
+
+
+// =====[ CONSTANTS ]=====
+
+#define TPANGLEFIX_OPTION_NAME "GOKZ - TPAngleFix"
+#define TPANGLEFIX_OPTION_DESCRIPTION "TPAngleFix - 0 = Disabled, 1 = Enabled"
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_gokz_tpanglefix =
+{
+ name = "gokz-tpanglefix",
+ file = "gokz-tpanglefix.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+}; \ No newline at end of file
diff --git a/sourcemod/scripting/include/gokz/version.inc b/sourcemod/scripting/include/gokz/version.inc
new file mode 100644
index 0000000..34cdb19
--- /dev/null
+++ b/sourcemod/scripting/include/gokz/version.inc
@@ -0,0 +1,12 @@
+/*
+ GOKZ Version Include
+
+ Website: https://bitbucket.org/kztimerglobalteam/gokz
+
+ You should not need to edit this file.
+ This file is overwritten in the build pipeline for versioning.
+*/
+
+
+
+#define GOKZ_VERSION "" \ No newline at end of file
diff --git a/sourcemod/scripting/include/json.inc b/sourcemod/scripting/include/json.inc
new file mode 100644
index 0000000..ebc46e4
--- /dev/null
+++ b/sourcemod/scripting/include/json.inc
@@ -0,0 +1,473 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2019 James Dickens. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_included
+ #endinput
+#endif
+#define _json_included
+
+#include <string>
+#include <json/definitions>
+#include <json/helpers/decode>
+#include <json/helpers/encode>
+#include <json/helpers/string>
+#include <json/object>
+
+/**
+ * Encodes a JSON instance into its string representation.
+ *
+ * @param obj Object to encode.
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ * @param pretty_print Should the output be pretty printed (newlines and spaces)? [default: false]
+ * @param depth The current depth of the encoder. [default: 0]
+ */
+stock void json_encode(
+ JSON_Object obj,
+ char[] output,
+ int max_size,
+ bool pretty_print = false,
+ int depth = 0
+)
+{
+ bool is_array = obj.IsArray;
+ bool is_empty = true;
+ int builder_size;
+
+ // used in key iterator
+ int str_length = 1;
+ int int_value;
+ int cell_length = 0;
+
+ StringMapSnapshot snap = null;
+ int json_size = 0;
+ if (is_array) {
+ json_size = obj.CurrentIndex;
+
+ strcopy(output, max_size, "[");
+ } else {
+ snap = obj.Snapshot();
+ json_size = snap.Length;
+
+ strcopy(output, max_size, "{");
+ }
+
+ int key_length = 0;
+ for (int i = 0; i < json_size; ++i) {
+ key_length = (is_array) ? JSON_INDEX_BUFFER_SIZE : snap.KeyBufferSize(i);
+ char[] key = new char[key_length];
+
+ if (is_array) {
+ obj.GetIndexString(key, key_length, i);
+ } else {
+ snap.GetKey(i, key, key_length);
+ }
+
+ // skip meta-keys
+ if (json_is_meta_key(key)) {
+ continue;
+ }
+
+ // skip keys that are marked as hidden
+ if (obj.GetKeyHidden(key)) {
+ continue;
+ }
+
+ JSON_CELL_TYPE type = obj.GetKeyType(key);
+ // skip keys of unknown type
+ if (type == Type_Invalid) {
+ continue;
+ }
+
+ // if we are dealing with a string, prepare the str_value variable for fetching
+ if (type == Type_String) {
+ str_length = obj.GetKeyLength(key);
+ }
+ char[] str_value = new char[str_length + 1];
+
+ // determine the length of the char[] needed to represent our cell data
+ cell_length = 0;
+ switch (type) {
+ case Type_String: {
+ // get the string value early, as its cell_length is determined by its contents
+ obj.GetString(key, str_value, str_length + 1);
+ cell_length = json_cell_string_size(str_length);
+ }
+ case Type_Int: {
+ // get the int value early, as its cell_length is determined by its contents
+ int_value = obj.GetInt(key);
+ cell_length = json_cell_int_size(int_value);
+ }
+ case Type_Float: {
+ cell_length = json_cell_float_size();
+ }
+ case Type_Bool: {
+ cell_length = json_cell_bool_size();
+ }
+ case Type_Null: {
+ cell_length = json_cell_null_size();
+ }
+ case Type_Object: {
+ cell_length = max_size;
+ }
+ }
+
+ // fit the contents into the cell
+ char[] cell = new char[cell_length];
+ switch (type) {
+ case Type_String: {
+ json_cell_string(str_value, cell, cell_length);
+ }
+ case Type_Int: {
+ json_cell_int(int_value, cell, cell_length);
+ }
+ case Type_Float: {
+ float value = obj.GetFloat(key);
+ json_cell_float(value, cell, cell_length);
+ }
+ case Type_Bool: {
+ bool value = obj.GetBool(key);
+ json_cell_bool(value, cell, cell_length);
+ }
+ case Type_Null: {
+ json_cell_null(cell, cell_length);
+ }
+ case Type_Object: {
+ JSON_Object child = obj.GetObject(key);
+ json_encode(child, cell, cell_length, pretty_print, depth + 1);
+ }
+ }
+
+ // make the builder fit our key:value
+ // use previously determined cell length and + 1 for ,
+ builder_size = cell_length + 1;
+ if (! is_array) {
+ // get the length of the key and + 1 for :
+ builder_size += json_cell_string_size(strlen(key)) + 1;
+
+ if (pretty_print) {
+ builder_size += strlen(JSON_PP_AFTER_COLON);
+ }
+ }
+
+ char[] builder = new char[builder_size];
+ strcopy(builder, builder_size, "");
+
+ // add the key if we're working with an object
+ if (! is_array) {
+ json_cell_string(key, builder, builder_size);
+ StrCat(builder, builder_size, ":");
+
+ if (pretty_print) {
+ StrCat(builder, builder_size, JSON_PP_AFTER_COLON);
+ }
+ }
+
+ // add the value and a trailing comma
+ StrCat(builder, builder_size, cell);
+ StrCat(builder, builder_size, ",");
+
+ // prepare pretty printing then send builder to output afterwards
+ if (pretty_print) {
+ StrCat(output, max_size, JSON_PP_NEWLINE);
+
+ for (int j = 0; j < depth + 1; ++j) {
+ StrCat(output, max_size, JSON_PP_INDENT);
+ }
+ }
+
+ StrCat(output, max_size, builder);
+
+ is_empty = false;
+ }
+
+ if (snap != null) {
+ delete snap;
+ }
+
+ if (! is_empty) {
+ // remove the final comma
+ output[strlen(output) - 1] = '\0';
+
+ if (pretty_print) {
+ StrCat(output, max_size, JSON_PP_NEWLINE);
+
+ for (int j = 0; j < depth; ++j) {
+ StrCat(output, max_size, JSON_PP_INDENT);
+ }
+ }
+ }
+
+ // append closing bracket
+ StrCat(output, max_size, (is_array) ? "]" : "}");
+}
+
+/**
+ * Decodes a JSON string into a JSON instance.
+ *
+ * @param buffer Buffer to decode.
+ * @param result Object to store output in. Setting this allows loading over
+ * an existing JSON instance, 'refreshing' it as opposed to
+ * creating a new one. [default: null]
+ * @param pos Current position of the decoder as a bytes offset into the buffer.
+ * @param depth Current depth of the decoder as child elements in the object.
+ * @returns JSON instance or null if decoding failed (buffer didn't contain valid JSON).
+ */
+stock JSON_Object json_decode(
+ const char[] buffer,
+ JSON_Object result = null,
+ int &pos = 0,
+ int depth = 0
+)
+{
+ int length = strlen(buffer);
+ bool is_array = false;
+
+ // skip preceding whitespace
+ if (! json_skip_whitespace(buffer, length, pos)) {
+ LogError("json_decode: buffer ended early at position %d", pos);
+
+ return null;
+ }
+
+ if (json_is_object(buffer[pos])) {
+ is_array = false;
+ } else if (json_is_array(buffer[pos])) {
+ is_array = true;
+ } else {
+ LogError("json_decode: character not identified as object or array at position %d", pos);
+
+ return null;
+ }
+
+ if (result == null) {
+ result = new JSON_Object(is_array);
+ }
+
+ bool empty_checked = false;
+ char[] key = new char[length];
+ char[] cell = new char[length];
+
+ // while we haven't reached the end of our structure
+ while (! is_array && ! json_is_object_end(buffer[pos])
+ || is_array && ! json_is_array_end(buffer[pos])) {
+ // pos is either an opening structure or comma, so increment past it
+ ++pos;
+
+ // skip any whitespace preceding the element
+ if (! json_skip_whitespace(buffer, length, pos)) {
+ LogError("json_decode: buffer ended early at position %d", pos);
+
+ return null;
+ }
+
+ // if we are at the end of an object or array
+ // and haven't checked for empty yet, we can stop here (empty structure)
+ if ((! is_array && json_is_object_end(buffer[pos])
+ || is_array && json_is_array_end(buffer[pos]))
+ && ! empty_checked) {
+ break;
+ }
+
+ empty_checked = true;
+
+ // if dealing with an object, look for the key
+ if (! is_array) {
+ if (! json_is_string(buffer[pos])) {
+ LogError("json_decode: expected key string at position %d", pos);
+
+ return null;
+ }
+
+ // extract the key from the buffer
+ json_extract_string(buffer, length, pos, key, length, is_array);
+
+ // skip any whitespace following the key
+ if (! json_skip_whitespace(buffer, length, pos)) {
+ LogError("json_decode: buffer ended early at position %d", pos);
+
+ return null;
+ }
+
+ // ensure that we find a colon
+ if (buffer[pos++] != ':') {
+ LogError("json_decode: expected colon after key at position %d", pos);
+
+ return null;
+ }
+
+ // skip any whitespace following the colon
+ if (! json_skip_whitespace(buffer, length, pos)) {
+ LogError("json_decode: buffer ended early at position %d", pos);
+
+ return null;
+ }
+ }
+
+ if (json_is_object(buffer[pos]) || json_is_array(buffer[pos])) {
+ // if we are dealing with an object or array
+ // fetch the existing object if one exists at the key
+ JSON_Object current = (! is_array) ? result.GetObject(key) : null;
+
+ // decode recursively
+ JSON_Object value = json_decode(buffer, current, pos, depth + 1);
+
+ // decoding failed, error will be logged in json_decode
+ if (value == null) {
+ return null;
+ }
+
+ if (is_array) {
+ result.PushObject(value);
+ } else {
+ result.SetObject(key, value);
+ }
+ } else if (json_is_string(buffer[pos])) {
+ // if we are dealing with a string, attempt to extract it
+ if (! json_extract_string(buffer, length, pos, cell, length, is_array)) {
+ LogError("json_decode: couldn't extract string at position %d", pos);
+
+ return null;
+ }
+
+ if (is_array) {
+ result.PushString(cell);
+ } else {
+ result.SetString(key, cell);
+ }
+ } else {
+ if (! json_extract_until_end(buffer, length, pos, cell, length, is_array)) {
+ LogError("json_decode: couldn't extract until end at position %d", pos);
+
+ return null;
+ }
+
+ if (strlen(cell) == 0) {
+ LogError("json_decode: empty cell encountered at position %d", pos);
+
+ return null;
+ }
+
+ if (json_is_int(cell)) {
+ int value = json_extract_int(cell);
+ if (is_array) {
+ result.PushInt(value);
+ } else {
+ result.SetInt(key, value);
+ }
+ } else if (json_is_float(cell)) {
+ float value = json_extract_float(cell);
+ if (is_array) {
+ result.PushFloat(value);
+ } else {
+ result.SetFloat(key, value);
+ }
+ } else if (json_is_bool(cell)) {
+ bool value = json_extract_bool(cell);
+ if (is_array) {
+ result.PushBool(value);
+ } else {
+ result.SetBool(key, value);
+ }
+ } else if (json_is_null(cell)) {
+ if (is_array) {
+ result.PushHandle(null);
+ } else {
+ result.SetHandle(key, null);
+ }
+ } else {
+ LogError("json_decode: unknown type encountered at position %d: %s", pos, cell);
+
+ return null;
+ }
+ }
+
+ if (! json_skip_whitespace(buffer, length, pos)) {
+ LogError("json_decode: buffer ended early at position %d", pos);
+
+ return null;
+ }
+ }
+
+ // skip remaining whitespace and ensure we're at the end of the buffer
+ ++pos;
+ if (json_skip_whitespace(buffer, length, pos) && depth == 0) {
+ LogError("json_decode: unexpected data after end of structure at position %d", pos);
+
+ return null;
+ }
+
+ return result;
+}
+
+/**
+ * Recursively cleans up a JSON instance and any JSON instances stored within.
+ *
+ * @param obj JSON instance to clean up.
+ */
+stock void json_cleanup(JSON_Object obj)
+{
+ bool is_array = obj.IsArray;
+
+ int key_length = 0;
+ StringMapSnapshot snap = obj.Snapshot();
+ for (int i = 0; i < snap.Length; ++i) {
+ key_length = snap.KeyBufferSize(i);
+ char[] key = new char[key_length];
+
+ // ignore meta keys
+ snap.GetKey(i, key, key_length);
+ if (json_is_meta_key(key)) {
+ continue;
+ }
+
+ // only clean up objects
+ JSON_CELL_TYPE type = obj.GetKeyType(key);
+ if (type != Type_Object) {
+ continue;
+ }
+
+ JSON_Object nested_obj = obj.GetObject(key);
+ if (nested_obj != null) {
+ nested_obj.Cleanup();
+ delete nested_obj;
+ }
+ }
+
+ obj.Clear();
+ delete snap;
+
+ if (is_array) {
+ obj.SetValue(JSON_ARRAY_INDEX_KEY, 0);
+ }
+}
diff --git a/sourcemod/scripting/include/json/decode_helpers.inc b/sourcemod/scripting/include/json/decode_helpers.inc
new file mode 100644
index 0000000..0032cc3
--- /dev/null
+++ b/sourcemod/scripting/include/json/decode_helpers.inc
@@ -0,0 +1,312 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2018 James D. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_decode_helpers_included
+ #endinput
+#endif
+#define _json_decode_helpers_included
+
+#include <string>
+
+/**
+ * @section Analysing format of incoming JSON cells.
+ */
+
+/**
+ * Checks whether the character at the given
+ * position in the buffer is whitespace.
+ *
+ * @param buffer String buffer of data.
+ * @param pos Position to check in buffer.
+ * @return True if buffer[pos] is whitespace, false otherwise.
+ */
+stock bool json_is_whitespace(const char[] buffer, int &pos) {
+ return buffer[pos] == ' ' || buffer[pos] == '\t' ||
+ buffer[pos] == '\r' || buffer[pos] == '\n';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of a string.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer[0] is the start of a string, false otherwise.
+ */
+stock bool json_is_string(const char[] buffer) {
+ return buffer[0] == '"';
+}
+
+/**
+ * Checks whether the buffer provided contains an int.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer contains an int, false otherwise.
+ */
+stock bool json_is_int(const char[] buffer) {
+ int length = strlen(buffer);
+ if (buffer[0] != '+' && buffer[0] != '-' && !IsCharNumeric(buffer[0])) {
+ return false;
+ }
+
+ for (int i = 0; i < length; ++i) {
+ if (!IsCharNumeric(buffer[i])) return false;
+ }
+
+ return true;
+}
+
+/**
+ * Checks whether the buffer provided contains a float.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer contains a float, false otherwise.
+ */
+stock bool json_is_float(const char[] buffer) {
+ bool has_decimal = false;
+ int length = strlen(buffer);
+ if (buffer[0] != '+' && buffer[0] != '-' && buffer[0] != '.' && !IsCharNumeric(buffer[0])) {
+ return false;
+ }
+
+ for (int i = 0; i < length; ++i) {
+ if (buffer[i] == '.') {
+ if (has_decimal) {
+ return false;
+ }
+
+ has_decimal = true;
+ } else if (!IsCharNumeric(buffer[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Checks whether the buffer provided contains a bool.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer contains a bool, false otherwise.
+ */
+stock bool json_is_bool(const char[] buffer) {
+ return StrEqual(buffer, "true") ||
+ StrEqual(buffer, "false");
+}
+
+/**
+ * Checks whether the buffer provided contains null.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer contains null, false otherwise.
+ */
+stock bool json_is_null(const char[] buffer) {
+ return StrEqual(buffer, "null");
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of an object.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer[0] is the start of an object, false otherwise.
+ */
+stock bool json_is_object(const char[] buffer) {
+ return buffer[0] == '{';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the end of an object.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer[0] is the end of an object, false otherwise.
+ */
+stock bool json_is_object_end(const char[] buffer) {
+ return buffer[0] == '}';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of an array.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer[0] is the start of an array, false otherwise.
+ */
+stock bool json_is_array(const char[] buffer) {
+ return buffer[0] == '[';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of an array.
+ *
+ * @param buffer String buffer of data.
+ * @return True if buffer[0] is the start of an array, false otherwise.
+ */
+stock bool json_is_array_end(const char[] buffer) {
+ return buffer[0] == ']';
+}
+
+/**
+ * Checks whether the character at the given position in the buffer
+ * is considered a valid 'end point' for some data, such as a
+ * colon (indicating a key), a comma (indicating a new element),
+ * or the end of an object or array.
+ *
+ * @param buffer String buffer of data.
+ * @param pos Position to check in buffer.
+ * @return True if buffer[pos] is a valid data end point, false otherwise.
+ */
+stock bool json_is_at_end(const char[] buffer, int &pos, bool is_array) {
+ return buffer[pos] == ',' ||
+ (!is_array && buffer[pos] == ':') ||
+ json_is_object_end(buffer[pos]) ||
+ json_is_array_end(buffer[pos]);
+}
+
+/**
+ * Moves the position until it reaches a non-whitespace
+ * character or the end of the buffer's maximum size.
+ *
+ * @param buffer String buffer of data.
+ * @param maxlen Maximum size of string buffer.
+ * @param pos Position to increment.
+ * @return True if pos is not at the end of the buffer, false otherwise.
+ */
+stock bool json_skip_whitespace(const char[] buffer, int maxlen, int &pos) {
+ while (json_is_whitespace(buffer, pos) && pos < maxlen) {
+ ++pos;
+ }
+
+ return pos < maxlen;
+}
+
+/**
+ * Extracts a JSON cell from the buffer until
+ * a valid end point is reached.
+ *
+ * @param buffer String buffer of data.
+ * @param maxlen Maximum size of string buffer.
+ * @param pos Position to increment.
+ * @param output String buffer to store output.
+ * @param output_maxlen Maximum size of output string buffer.
+ * @param is_array Whether the decoder is currently processing an array.
+ * @return True if pos is not at the end of the buffer, false otherwise.
+ */
+stock bool json_extract_until_end(const char[] buffer, int maxlen, int &pos, char[] output, int output_maxlen, bool is_array) {
+ // extracts a string from current pos until a valid 'end point'
+ strcopy(output, output_maxlen, "");
+
+ int start = pos;
+ while (!json_is_whitespace(buffer, pos) && !json_is_at_end(buffer, pos, is_array) && pos < maxlen) {
+ ++pos;
+ }
+ int end = pos - 1;
+
+ // skip trailing whitespace
+ json_skip_whitespace(buffer, maxlen, pos);
+
+ if (!json_is_at_end(buffer, pos, is_array)) return false;
+ strcopy(output, end - start + 2, buffer[start]);
+
+ return pos < maxlen;
+}
+
+
+/**
+ * Extracts a JSON string from the buffer until
+ * a valid end point is reached.
+ *
+ * @param buffer String buffer of data.
+ * @param maxlen Maximum size of string buffer.
+ * @param pos Position to increment.
+ * @param output String buffer to store output.
+ * @param output_maxlen Maximum size of output string buffer.
+ * @param is_array Whether the decoder is currently processing an array.
+ * @return True if pos is not at the end of the buffer, false otherwise.
+ */
+stock bool json_extract_string(const char[] buffer, int maxlen, int &pos, char[] output, int output_maxlen, bool is_array) {
+ // extracts a string which needs to be quote-escaped
+ strcopy(output, output_maxlen, "");
+
+ ++pos;
+ int start = pos;
+ while (!(buffer[pos] == '"' && buffer[pos - 1] != '\\') && pos < maxlen) {
+ ++pos;
+ }
+ int end = pos - 1;
+
+ // jump 1 ahead since we ended on " instead of an ending char
+ ++pos;
+
+ // skip trailing whitespace
+ json_skip_whitespace(buffer, maxlen, pos);
+
+ if (!json_is_at_end(buffer, pos, is_array)) return false;
+ // copy only from start with length end - start + 2 (+2 for NULL terminator and something else)
+ strcopy(output, end - start + 2, buffer[start]);
+ json_unescape_string(output, maxlen);
+
+ return pos < maxlen;
+}
+
+/**
+ * Extracts an int from the buffer.
+ *
+ * @param buffer String buffer of data.
+ * @return Int value of the buffer.
+ */
+stock int json_extract_int(const char[] buffer) {
+ return StringToInt(buffer);
+}
+
+/**
+ * Extracts a float from the buffer.
+ *
+ * @param buffer String buffer of data.
+ * @return Float value of the buffer.
+ */
+stock float json_extract_float(const char[] buffer) {
+ return StringToFloat(buffer);
+}
+
+/**
+ * Extracts a bool from the buffer.
+ *
+ * @param buffer String buffer of data.
+ * @return Bool value of the buffer.
+ */
+stock bool json_extract_bool(const char[] buffer) {
+ return StrEqual(buffer, "true");
+}
diff --git a/sourcemod/scripting/include/json/definitions.inc b/sourcemod/scripting/include/json/definitions.inc
new file mode 100644
index 0000000..63063d3
--- /dev/null
+++ b/sourcemod/scripting/include/json/definitions.inc
@@ -0,0 +1,103 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2019 James Dickens. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_definitions_included
+ #endinput
+#endif
+#define _json_definitions_included
+
+#include <string>
+#include <json/helpers/string>
+
+/**
+ * @section Pretty Print Constants
+ *
+ * Used to determine how pretty printed JSON should be formatted when encoded.
+ * You can modify these if you prefer your JSON formatted differently.
+ */
+
+#define JSON_PP_AFTER_COLON " "
+#define JSON_PP_INDENT " "
+#define JSON_PP_NEWLINE "\n"
+
+/**
+ * @section Buffer Size Constants
+ *
+ * You may need to change these if you are working with very large arrays or floating point numbers.
+ */
+
+#define JSON_FLOAT_BUFFER_SIZE 32
+#define JSON_INDEX_BUFFER_SIZE 16
+
+/**
+ * @section Meta-key Constants
+ *
+ * Used to store metadata for each key in an object.
+ * You shouldn't need to change these unless working with keys that may clash with them.
+ */
+
+#define JSON_ARRAY_INDEX_KEY "__array_index"
+#define JSON_META_TYPE_KEY ":type"
+#define JSON_META_LENGTH_KEY ":length"
+#define JSON_META_HIDDEN_KEY ":hidden"
+
+/**
+ * @section General
+ */
+
+/**
+ * Types of cells within a JSON object
+ */
+enum JSON_CELL_TYPE {
+ Type_Invalid = -1,
+ Type_String = 0,
+ Type_Int,
+ Type_Float,
+ Type_Bool,
+ Type_Null,
+ Type_Object
+};
+
+/**
+ * Checks whether the key provided is a meta-key that should only be used internally.
+ *
+ * @param key Key to check.
+ * @returns True when it is a meta-key, false otherwise.
+ */
+stock bool json_is_meta_key(char[] key)
+{
+ return json_string_endswith(key, JSON_META_TYPE_KEY)
+ || json_string_endswith(key, JSON_META_LENGTH_KEY)
+ || json_string_endswith(key, JSON_META_HIDDEN_KEY)
+ || StrEqual(key, JSON_ARRAY_INDEX_KEY);
+}
diff --git a/sourcemod/scripting/include/json/encode_helpers.inc b/sourcemod/scripting/include/json/encode_helpers.inc
new file mode 100644
index 0000000..37cb83d
--- /dev/null
+++ b/sourcemod/scripting/include/json/encode_helpers.inc
@@ -0,0 +1,164 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2018 James D. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_encode_helpers_included
+ #endinput
+#endif
+#define _json_encode_helpers_included
+
+#include <string>
+
+/**
+ * @section Calculating buffer sizes for JSON cells.
+ */
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of a string.
+ *
+ * @param maxlen The string's current length or buffer size.
+ * @return Maximum buffer length.
+ */
+stock int json_cell_string_size(int maxlen) {
+ return (maxlen * 2) + 3; // * 2 for potential escaping, + 2 for surrounding quotes + NULL
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of an int.
+ *
+ * @param input The int.
+ * @return Maximum buffer length.
+ */
+stock int json_cell_int_size(int input) {
+ if (input == 0) {
+ return 2; // "0" + NULL
+ }
+
+ return ((input < 0) ? 1 : 0) + RoundToFloor(Logarithm(FloatAbs(float(input)), 10.0)) + 2; // neg sign + number of digits + NULL
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of a float.
+ *
+ * @return Maximum buffer length.
+ */
+stock int json_cell_float_size() {
+ return JSON_FLOAT_BUFFER_SIZE; // fixed-length
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of a bool.
+ *
+ * @return Maximum buffer length.
+ */
+stock int json_cell_bool_size() {
+ return 6; // "true|false" + NULL
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of null.
+ *
+ * @return Maximum buffer length.
+ */
+stock int json_cell_null_size() {
+ return 5; // "null" + NULL
+}
+
+/**
+ * @section Generating JSON cells.
+ */
+
+/**
+ * Generates the JSON cell representation of a string.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param maxlen Maximum size of string buffer.
+ */
+stock void json_cell_string(const char[] input, char[] output, int maxlen) {
+ strcopy(output, maxlen, "_"); // add dummy char at start so first quotation isn't escaped
+ StrCat(output, maxlen, input); // add input string to output
+ // escape everything according to JSON spec
+ json_escape_string(output, maxlen);
+
+ // surround string with quotations
+ output[0] = '"';
+ StrCat(output, maxlen, "\"");
+}
+
+/**
+ * Generates the JSON cell representation of an int.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param maxlen Maximum size of string buffer.
+ */
+stock void json_cell_int(int input, char[] output, int maxlen) {
+ IntToString(input, output, maxlen);
+}
+
+/**
+ * Generates the JSON cell representation of a float.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param maxlen Maximum size of string buffer.
+ */
+stock void json_cell_float(float input, char[] output, int maxlen) {
+ FloatToString(input, output, maxlen);
+}
+
+/**
+ * Generates the JSON cell representation of a bool.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param maxlen Maximum size of string buffer.
+ */
+stock void json_cell_bool(bool input, char[] output, int maxlen) {
+ strcopy(output, maxlen, (input) ? "true" : "false");
+}
+
+/**
+ * Generates the JSON cell representation of null.
+ *
+ * @param output String buffer to store output.
+ * @param maxlen Maximum size of string buffer.
+ */
+stock void json_cell_null(char[] output, int maxlen) {
+ strcopy(output, maxlen, "null");
+}
diff --git a/sourcemod/scripting/include/json/helpers/decode.inc b/sourcemod/scripting/include/json/helpers/decode.inc
new file mode 100644
index 0000000..f420222
--- /dev/null
+++ b/sourcemod/scripting/include/json/helpers/decode.inc
@@ -0,0 +1,502 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2019 James Dickens. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_helpers_decode_included
+ #endinput
+#endif
+#define _json_helpers_decode_included
+
+#include <string>
+
+/**
+ * @section Determine Buffer Contents
+ */
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is whitespace.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer
+ * is whitespace, false otherwise.
+ */
+stock bool json_is_whitespace(const char[] buffer)
+{
+ return buffer[0] == ' '
+ || buffer[0] == '\t'
+ || buffer[0] == '\r'
+ || buffer[0] == '\n';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of a string.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer
+ * is the start of a string, false otherwise.
+ */
+stock bool json_is_string(const char[] buffer)
+{
+ return buffer[0] == '"';
+}
+
+/**
+ * Checks whether the buffer provided contains an int.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if buffer contains an int, false otherwise.
+ */
+stock bool json_is_int(const char[] buffer)
+{
+ bool starts_with_zero = false;
+ bool has_digit_gt_zero = false;
+
+ int length = strlen(buffer);
+ for (int i = 0; i < length; ++i) {
+ // allow minus as first character only
+ if (i == 0 && buffer[i] == '-') {
+ continue;
+ }
+
+ if (IsCharNumeric(buffer[i])) {
+ if (buffer[i] == '0') {
+ if (starts_with_zero) {
+ // detect repeating leading zeros
+ return false;
+ } else if (! has_digit_gt_zero) {
+ starts_with_zero = true;
+ }
+ } else {
+ has_digit_gt_zero = true;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ // buffer must start with zero and have no other numerics before decimal
+ // OR not start with zero and have other numerics
+ return (starts_with_zero && ! has_digit_gt_zero)
+ || (! starts_with_zero && has_digit_gt_zero);
+}
+
+/**
+ * Checks whether the buffer provided contains a float.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if buffer contains a float, false otherwise.
+ */
+stock bool json_is_float(const char[] buffer)
+{
+ bool starts_with_zero = false;
+ bool has_digit_gt_zero = false;
+ bool after_decimal = false;
+ bool has_digit_after_decimal = false;
+ bool after_exponent = false;
+ bool has_digit_after_exponent = false;
+
+ int length = strlen(buffer);
+ for (int i = 0; i < length; ++i) {
+ // allow minus as first character only
+ if (i == 0 && buffer[i] == '-') {
+ continue;
+ }
+
+ // if we haven't encountered a decimal or exponent yet
+ if (! after_decimal && ! after_exponent) {
+ if (buffer[i] == '.') {
+ // if we encounter a decimal before any digits
+ if (! starts_with_zero && ! has_digit_gt_zero) {
+ return false;
+ }
+
+ after_decimal = true;
+ } else if (buffer[i] == 'e' || buffer[i] == 'E') {
+ // if we encounter an exponent before any non-zero digits
+ if (starts_with_zero && ! has_digit_gt_zero) {
+ return false;
+ }
+
+ after_exponent = true;
+ } else if (IsCharNumeric(buffer[i])) {
+ if (buffer[i] == '0') {
+ if (starts_with_zero) {
+ // detect repeating leading zeros
+ return false;
+ } else if (! has_digit_gt_zero) {
+ starts_with_zero = true;
+ }
+ } else {
+ has_digit_gt_zero = true;
+ }
+ } else {
+ return false;
+ }
+ } else if (after_decimal && ! after_exponent) {
+ // after decimal has been encountered, allow any numerics
+ if (IsCharNumeric(buffer[i])) {
+ has_digit_after_decimal = true;
+ } else if (buffer[i] == 'e' || buffer[i] == 'E') {
+ if (! has_digit_after_decimal) {
+ // detect exponents directly after decimal
+ return false;
+ }
+
+ after_exponent = true;
+ } else {
+ return false;
+ }
+ } else if (after_exponent) {
+ if (
+ (buffer[i] == '+' || buffer[i] == '-')
+ && (buffer[i - 1] == 'e' || buffer[i - 1] == 'E')
+ ) {
+ // allow + or - directly after exponent
+ continue;
+ } else if (IsCharNumeric(buffer[i])) {
+ has_digit_after_exponent = true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ if (starts_with_zero && has_digit_gt_zero) {
+ /* if buffer starts with zero, there should
+ be no other digits before the decimal */
+ return false;
+ }
+
+ // if we have a decimal, there should be digit(s) after it
+ if (after_decimal) {
+ if (! has_digit_after_decimal) {
+ return false;
+ }
+ }
+
+ // if we have an exponent, there should be digit(s) after it
+ if (after_exponent) {
+ if (! has_digit_after_exponent) {
+ return false;
+ }
+ }
+
+ /* we should have reached an exponent, decimal, or both.
+ otherwise, this number can be handled by the int parser */
+ return after_decimal || after_exponent;
+}
+
+/**
+ * Checks whether the buffer provided contains a bool.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if buffer contains a bool, false otherwise.
+ */
+stock bool json_is_bool(const char[] buffer)
+{
+ return StrEqual(buffer, "true") || StrEqual(buffer, "false");
+}
+
+/**
+ * Checks whether the buffer provided contains null.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if buffer contains null, false otherwise.
+ */
+stock bool json_is_null(const char[] buffer)
+{
+ return StrEqual(buffer, "null");
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of an object.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer is
+ * the start of an object, false otherwise.
+ */
+stock bool json_is_object(const char[] buffer)
+{
+ return buffer[0] == '{';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the end of an object.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer is
+ * the end of an object, false otherwise.
+ */
+stock bool json_is_object_end(const char[] buffer)
+{
+ return buffer[0] == '}';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the start of an array.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer is
+ * the start of an array, false otherwise.
+ */
+stock bool json_is_array(const char[] buffer)
+{
+ return buffer[0] == '[';
+}
+
+/**
+ * Checks whether the character at the beginning
+ * of the buffer is the end of an array.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer is
+ * the end of an array, false otherwise.
+ */
+stock bool json_is_array_end(const char[] buffer)
+{
+ return buffer[0] == ']';
+}
+
+/**
+ * Checks whether the character at the beginning of the buffer
+ * is considered a valid 'end point' for some data, such as a
+ * colon (indicating a key), a comma (indicating a new element),
+ * or the end of an object or array.
+ *
+ * @param buffer String buffer of data.
+ * @returns True if the first character in the buffer
+ * is a valid data end point, false otherwise.
+ */
+stock bool json_is_at_end(const char[] buffer, bool is_array)
+{
+ return buffer[0] == ','
+ || (! is_array && buffer[0] == ':')
+ || json_is_object_end(buffer[0])
+ || json_is_array_end(buffer[0]);
+}
+
+/**
+ * @section Extract Contents from Buffer
+ */
+
+/**
+ * Moves the position until it reaches a non-whitespace
+ * character or the end of the buffer's maximum size.
+ *
+ * @param buffer String buffer of data.
+ * @param max_size Maximum size of string buffer.
+ * @param pos Position to increment.
+ * @returns True if pos has not reached the end
+ * of the buffer, false otherwise.
+ */
+stock bool json_skip_whitespace(const char[] buffer, int max_size, int &pos)
+{
+ while (json_is_whitespace(buffer[pos]) && pos < max_size) {
+ ++pos;
+ }
+
+ return pos < max_size;
+}
+
+/**
+ * Extracts a JSON cell from the buffer until
+ * a valid end point is reached.
+ *
+ * @param buffer String buffer of data.
+ * @param max_size Maximum size of string buffer.
+ * @param pos Position to increment.
+ * @param output String buffer to store output.
+ * @param output_max_size Maximum size of output string buffer.
+ * @param is_array Whether the decoder is processing an array.
+ * @returns True if pos has not reached the end
+ * of the buffer, false otherwise.
+ */
+stock bool json_extract_until_end(
+ const char[] buffer,
+ int max_size,
+ int &pos,
+ char[] output,
+ int output_max_size,
+ bool is_array
+) {
+ strcopy(output, output_max_size, "");
+
+ // set start to position of first character in cell
+ int start = pos;
+
+ // while we haven't hit whitespace, an end point or the end of the buffer
+ while (
+ ! json_is_whitespace(buffer[pos])
+ && ! json_is_at_end(buffer[pos], is_array)
+ && pos < max_size
+ ) {
+ ++pos;
+ }
+
+ // set end to the current position
+ int end = pos;
+
+ // skip any following whitespace
+ json_skip_whitespace(buffer, max_size, pos);
+
+ // if we aren't at a valid endpoint, extraction has failed
+ if (! json_is_at_end(buffer[pos], is_array)) {
+ return false;
+ }
+
+ // copy only from start with length end - start + NULL terminator
+ strcopy(output, end - start + 1, buffer[start]);
+
+ return pos < max_size;
+}
+
+/**
+ * Extracts a JSON string from the buffer until
+ * a valid end point is reached.
+ *
+ * @param buffer String buffer of data.
+ * @param max_size Maximum size of string buffer.
+ * @param pos Position to increment.
+ * @param output String buffer to store output.
+ * @param output_max_size Maximum size of output string buffer.
+ * @param is_array Whether the decoder is processing an array.
+ * @returns True if pos has not reached the end
+ * of the buffer, false otherwise.
+ */
+stock bool json_extract_string(
+ const char[] buffer,
+ int max_size,
+ int &pos,
+ char[] output,
+ int output_max_size,
+ bool is_array
+) {
+ strcopy(output, output_max_size, "");
+
+ // increment past opening quote
+ ++pos;
+
+ // set start to position of first character in string
+ int start = pos;
+
+ // while we haven't hit the end of the buffer
+ while (pos < max_size) {
+ // check for unescaped control characters
+ if (
+ buffer[pos] == '\b'
+ || buffer[pos] == '\f'
+ || buffer[pos] == '\n'
+ || buffer[pos] == '\r'
+ || buffer[pos] == '\t'
+ ) {
+ return false;
+ }
+
+ if (buffer[pos] == '"') {
+ // count preceding backslashes to check if quote is escaped
+ int search_pos = pos;
+ int preceding_backslashes = 0;
+ while (search_pos > 0 && buffer[--search_pos] == '\\') {
+ ++preceding_backslashes;
+ }
+
+ // if we have an even number of backslashes, the quote is not escaped
+ if (preceding_backslashes % 2 == 0) {
+ break;
+ }
+ }
+
+ // pass over the character as it is part of the string
+ ++pos;
+ }
+
+ // set end to the current position
+ int end = pos;
+
+ // increment past closing quote
+ ++pos;
+
+ // skip trailing whitespace
+ if (! json_skip_whitespace(buffer, max_size, pos)) {
+ return false;
+ }
+
+ // if we haven't reached an ending character at the end of the cell,
+ // there is likely junk data not encapsulated by a string
+ if (! json_is_at_end(buffer[pos], is_array)) {
+ return false;
+ }
+
+ // copy only from start with length end - start + NULL terminator
+ strcopy(output, end - start + 1, buffer[start]);
+ json_unescape_string(output, max_size);
+
+ return pos < max_size;
+}
+
+/**
+ * Extracts an int from the buffer.
+ *
+ * @param buffer String buffer of data.
+ * @returns Int value of the buffer.
+ */
+stock int json_extract_int(const char[] buffer)
+{
+ return StringToInt(buffer);
+}
+
+/**
+ * Extracts a float from the buffer.
+ *
+ * @param buffer String buffer of data.
+ * @returns Float value of the buffer.
+ */
+stock float json_extract_float(const char[] buffer)
+{
+ return StringToFloat(buffer);
+}
+
+/**
+ * Extracts a bool from the buffer.
+ *
+ * @param buffer String buffer of data.
+ * @returns Bool value of the buffer.
+ */
+stock bool json_extract_bool(const char[] buffer)
+{
+ return StrEqual(buffer, "true");
+}
diff --git a/sourcemod/scripting/include/json/helpers/encode.inc b/sourcemod/scripting/include/json/helpers/encode.inc
new file mode 100644
index 0000000..ceae54d
--- /dev/null
+++ b/sourcemod/scripting/include/json/helpers/encode.inc
@@ -0,0 +1,200 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2019 James Dickens. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_helpers_encode_included
+ #endinput
+#endif
+#define _json_helpers_encode_included
+
+#include <string>
+
+/**
+ * @section Calculate Buffer Size for Value
+ */
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of a string.
+ *
+ * @param length The length of the string.
+ * @returns Maximum buffer length.
+ */
+stock int json_cell_string_size(int length)
+{
+ // double for potential escaping, + 2 for outside quotes + NULL terminator
+ return (length * 2) + 3;
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of an int.
+ *
+ * @param input Value to calculate maximum buffer length for.
+ * @returns Maximum buffer length.
+ */
+stock int json_cell_int_size(int input)
+{
+ if (input == 0) {
+ // "0" + NULL terminator
+ return 2;
+ }
+
+ int result = 0;
+ if (input < 0) {
+ // negative sign
+ result += 1;
+ }
+
+ // calculate number of digits in number
+ result += RoundToFloor(Logarithm(FloatAbs(float(input)), 10.0)) + 1;
+
+ // NULL terminator
+ result += 1;
+
+ return result;
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of a float.
+ *
+ * @returns Maximum buffer length.
+ */
+stock int json_cell_float_size()
+{
+ return JSON_FLOAT_BUFFER_SIZE;
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of a bool.
+ *
+ * @returns Maximum buffer length.
+ */
+stock int json_cell_bool_size()
+{
+ // "true"|"false" + NULL terminator
+ return 6;
+}
+
+/**
+ * Calculates the maximum buffer length required to
+ * store the JSON cell representation of null.
+ *
+ * @returns Maximum buffer length.
+ */
+stock int json_cell_null_size()
+{
+ // "null" + NULL terminator
+ return 5;
+}
+
+/**
+ * @section Convert Values to JSON Cells
+ */
+
+/**
+ * Generates the JSON cell representation of a string.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_cell_string(const char[] input, char[] output, int max_size)
+{
+ // add dummy char that won't be escaped to replace with a quote later
+ strcopy(output, max_size, "?");
+
+ // add input string to output
+ StrCat(output, max_size, input);
+
+ // escape the output string
+ json_escape_string(output, max_size);
+
+ // surround string with quotations
+ output[0] = '"';
+ StrCat(output, max_size, "\"");
+}
+
+/**
+ * Generates the JSON cell representation of an int.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_cell_int(int input, char[] output, int max_size)
+{
+ IntToString(input, output, max_size);
+}
+
+/**
+ * Generates the JSON cell representation of a float.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_cell_float(float input, char[] output, int max_size)
+{
+ FloatToString(input, output, max_size);
+
+ // trim trailing 0s from float output up until decimal point
+ int last_char = strlen(output) - 1;
+ while (output[last_char] == '0' && output[last_char - 1] != '.') {
+ output[last_char--] = '\0';
+ }
+}
+
+/**
+ * Generates the JSON cell representation of a bool.
+ *
+ * @param input Value to generate output for.
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_cell_bool(bool input, char[] output, int max_size)
+{
+ strcopy(output, max_size, (input) ? "true" : "false");
+}
+
+/**
+ * Generates the JSON cell representation of null.
+ *
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_cell_null(char[] output, int max_size)
+{
+ strcopy(output, max_size, "null");
+}
diff --git a/sourcemod/scripting/include/json/helpers/string.inc b/sourcemod/scripting/include/json/helpers/string.inc
new file mode 100644
index 0000000..14fe38a
--- /dev/null
+++ b/sourcemod/scripting/include/json/helpers/string.inc
@@ -0,0 +1,133 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2019 James Dickens. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_helpers_string_included
+ #endinput
+#endif
+#define _json_helpers_string_included
+
+/**
+ * Mapping characters to their escaped form.
+ */
+char JSON_STRING_NORMAL[][] = {
+ "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t"
+};
+char JSON_STRING_ESCAPED[][] = {
+ "\\\\", "\\\"", "\\/", "\\b", "\\f", "\\n", "\\r", "\\t"
+};
+
+/**
+ * Escapes a string in-place in a buffer.
+ *
+ * @param buffer String buffer.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_escape_string(char[] buffer, int max_size)
+{
+ for (int i = 0; i < sizeof(JSON_STRING_NORMAL); ++i) {
+ ReplaceString(
+ buffer,
+ max_size,
+ JSON_STRING_NORMAL[i],
+ JSON_STRING_ESCAPED[i]
+ );
+ }
+}
+
+/**
+ * Unescapes a string in-place in a buffer.
+ *
+ * @param buffer String buffer.
+ * @param max_size Maximum size of string buffer.
+ */
+stock void json_unescape_string(char[] buffer, int max_size)
+{
+ for (int i = 0; i < sizeof(JSON_STRING_NORMAL); ++i) {
+ ReplaceString(
+ buffer,
+ max_size,
+ JSON_STRING_ESCAPED[i],
+ JSON_STRING_NORMAL[i]
+ );
+ }
+}
+
+/**
+ * Checks if a string starts with another string.
+ *
+ * @param haystack String to check that starts with needle.
+ * @param max_size Maximum size of string buffer.
+ * @param needle String to check that haystack starts with.
+ * @returns True if haystack begins with needle, false otherwise.
+ */
+stock bool json_string_startswith(const char[] haystack, const char[] needle)
+{
+ int haystack_length = strlen(haystack);
+ int needle_length = strlen(needle);
+ if (needle_length > haystack_length) {
+ return false;
+ }
+
+ for (int i = 0; i < needle_length; ++i) {
+ if (haystack[i] != needle[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Checks if a string ends with another string.
+ *
+ * @param haystack String to check that ends with needle.
+ * @param max_size Maximum size of string buffer.
+ * @param needle String to check that haystack ends with.
+ * @returns True if haystack ends with needle, false otherwise.
+ */
+stock bool json_string_endswith(const char[] haystack, const char[] needle)
+{
+ int haystack_length = strlen(haystack);
+ int needle_length = strlen(needle);
+ if (needle_length > haystack_length) {
+ return false;
+ }
+
+ for (int i = 0; i < needle_length; ++i) {
+ if (haystack[haystack_length - needle_length + i] != needle[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/sourcemod/scripting/include/json/object.inc b/sourcemod/scripting/include/json/object.inc
new file mode 100644
index 0000000..8d17568
--- /dev/null
+++ b/sourcemod/scripting/include/json/object.inc
@@ -0,0 +1,1014 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2019 James Dickens. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_object_included
+ #endinput
+#endif
+#define _json_object_included
+
+#include <string>
+#include <json/definitions>
+#include <json/helpers/encode>
+#include <json>
+
+methodmap JSON_Object < StringMap
+{
+ /**
+ * Creates a new JSON_Object.
+ *
+ * @param is_array Should the object created be an array? [default: false]
+ * @returns A new JSON_Object.
+ */
+ public JSON_Object(bool is_array = false)
+ {
+ StringMap self = CreateTrie();
+ if (is_array) {
+ self.SetValue(JSON_ARRAY_INDEX_KEY, 0);
+ }
+
+ return view_as<JSON_Object>(self);
+ }
+
+ /**
+ * Checks whether the object has a key.
+ *
+ * @param key Key to check existence of.
+ * @returns True if the key exists, false otherwise.
+ */
+ public bool HasKey(const char[] key)
+ {
+ int dummy_int;
+ char dummy_str[1];
+
+ return this.GetValue(key, dummy_int)
+ || this.GetString(key, dummy_str, sizeof(dummy_str));
+ }
+
+ /**
+ * @section Array helpers.
+ */
+
+ /**
+ * Whether the current object is an array.
+ */
+ property bool IsArray {
+ public get()
+ {
+ return this.HasKey(JSON_ARRAY_INDEX_KEY);
+ }
+ }
+
+ /**
+ * The current index of the object if it is an array, or -1 otherwise.
+ */
+ property int CurrentIndex {
+ public get()
+ {
+ if (! this.IsArray) {
+ return -1;
+ }
+
+ int result;
+ return (this.GetValue(JSON_ARRAY_INDEX_KEY, result)) ? result : -1;
+ }
+
+ public set(int value)
+ {
+ this.SetValue(JSON_ARRAY_INDEX_KEY, value);
+ }
+ }
+
+ /**
+ * The number of items in the object if it is an array,
+ * or the number of keys (including meta-keys) otherwise.
+ */
+ property int Length {
+ public get()
+ {
+ StringMapSnapshot snap = this.Snapshot();
+ int length = (this.IsArray) ? this.CurrentIndex : snap.Length;
+ delete snap;
+
+ return length;
+ }
+ }
+
+ /**
+ * Increments the current index of the object.
+ *
+ * @returns True on success, false if the current object is not an array.
+ */
+ public bool IncrementIndex()
+ {
+ if (! this.HasKey(JSON_ARRAY_INDEX_KEY)) {
+ return false;
+ }
+
+ this.CurrentIndex += 1;
+
+ return true;
+ }
+
+ /**
+ * Checks whether the object has an index.
+ *
+ * @param index Index to check existence of.
+ * @returns True if the index exists, false otherwise.
+ */
+ public bool HasIndex(int index)
+ {
+ return index >= 0 && index < this.Length;
+ }
+
+ /**
+ * Gets the string representation of an array index.
+ *
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ * @param key Key to get string for. [default: current index]
+ * @returns True on success, false otherwise.
+ */
+ public int GetIndexString(char[] output, int max_size, int key = -1)
+ {
+ key = (key == -1) ? this.CurrentIndex : key;
+ if (key == -1) {
+ return false;
+ }
+
+ return IntToString(key, output, max_size);
+ }
+
+ /**
+ * @section Internal Getters
+ */
+
+ /**
+ * Gets the cell type stored at a key.
+ *
+ * @param key Key to get value type for.
+ * @returns Value type for key provided,
+ * or Type_Invalid if it does not exist.
+ */
+ public JSON_CELL_TYPE GetKeyType(const char[] key)
+ {
+ int max_size = strlen(key) + strlen(JSON_META_TYPE_KEY) + 1;
+ char[] type_key = new char[max_size];
+ Format(type_key, max_size, "%s%s", key, JSON_META_TYPE_KEY);
+
+ JSON_CELL_TYPE type;
+ return (this.GetValue(type_key, type)) ? type : Type_Invalid;
+ }
+
+ /**
+ * Gets the cell type stored at an index.
+ *
+ * @param index Index to get value type for.
+ * @returns Value type for index provided, or Type_Invalid if it does not exist.
+ */
+ public JSON_CELL_TYPE GetKeyTypeIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return Type_Invalid;
+ }
+
+ return this.GetKeyType(key);
+ }
+
+ /**
+ * Gets the length of the string stored at a key.
+ *
+ * @param key Key to get string length for.
+ * @returns Length of string at key provided,
+ * or -1 if it is not a string/does not exist.
+ */
+ public int GetKeyLength(const char[] key)
+ {
+ int max_size = strlen(key) + strlen(JSON_META_LENGTH_KEY) + 1;
+ char[] length_key = new char[max_size];
+ Format(length_key, max_size, "%s%s", key, JSON_META_LENGTH_KEY);
+
+ int length;
+ return (this.GetValue(length_key, length)) ? length : -1;
+ }
+
+ /**
+ * Gets the length of the string stored at an index.
+ *
+ * @param index Index to get string length for.
+ * @returns Length of string at index provided, or -1 if it is not a string/does not exist.
+ */
+ public int GetKeyLengthIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return -1;
+ }
+
+ return this.GetKeyLength(key);
+ }
+
+ /**
+ * Gets whether the key should be hidden from encoding.
+ *
+ * @param key Key to get hidden state for.
+ * @returns Whether or not the key should be hidden.
+ */
+ public bool GetKeyHidden(const char[] key)
+ {
+ int max_size = strlen(key) + strlen(JSON_META_HIDDEN_KEY) + 1;
+ char[] length_key = new char[max_size];
+ Format(length_key, max_size, "%s%s", key, JSON_META_HIDDEN_KEY);
+
+ bool hidden;
+ return (this.GetValue(length_key, hidden)) ? hidden : false;
+ }
+
+ /**
+ * Gets whether the index should be hidden from encoding.
+ *
+ * @param index Index to get hidden state for.
+ * @returns Whether or not the index should be hidden.
+ */
+ public bool GetKeyHiddenIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.GetKeyHidden(key);
+ }
+
+ /**
+ * @section Internal Setters
+ */
+
+ /**
+ * Sets the cell type stored at a key.
+ *
+ * @param key Key to set value type for.
+ * @param type Type to set key to.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetKeyType(const char[] key, JSON_CELL_TYPE type)
+ {
+ int max_size = strlen(key) + strlen(JSON_META_TYPE_KEY) + 1;
+ char[] type_key = new char[max_size];
+ Format(type_key, max_size, "%s%s", key, JSON_META_TYPE_KEY);
+
+ return this.SetValue(type_key, type);
+ }
+
+ /**
+ * Sets the cell type stored at an index.
+ *
+ * @param index Index to set value type for.
+ * @param type Type to set index to.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetKeyTypeIndexed(int index, JSON_CELL_TYPE value)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetKeyType(key, value);
+ }
+
+ /**
+ * Sets the length of the string stored at a key.
+ *
+ * @param key Key to set string length for.
+ * @param length Length to set string to.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetKeyLength(const char[] key, int length)
+ {
+ int max_size = strlen(key) + strlen(JSON_META_LENGTH_KEY) + 1;
+ char[] length_key = new char[max_size];
+ Format(length_key, max_size, "%s%s", key, JSON_META_LENGTH_KEY);
+
+ return this.SetValue(length_key, length);
+ }
+
+ /**
+ * Sets the length of the string stored at an index.
+ *
+ * @param index Index to set string length for.
+ * @param length Length to set string to.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetKeyLengthIndexed(int index, int length)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetKeyLength(key, length);
+ }
+
+ /**
+ * Sets whether the key should be hidden from encoding.
+ *
+ * @param key Key to set hidden state for.
+ * @param hidden Wheter or not the key should be hidden.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetKeyHidden(const char[] key, bool hidden)
+ {
+ int max_size = strlen(key) + strlen(JSON_META_HIDDEN_KEY) + 1;
+ char[] hidden_key = new char[max_size];
+ Format(hidden_key, max_size, "%s%s", key, JSON_META_HIDDEN_KEY);
+
+ return this.SetValue(hidden_key, hidden);
+ }
+
+ /**
+ * Sets whether the index should be hidden from encoding.
+ *
+ * @param index Index to set hidden state for.
+ * @param hidden Wheter or not the index should be hidden.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetKeyHiddenIndexed(int index, bool hidden)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetKeyHidden(key, hidden);
+ }
+
+ /**
+ * @section Getters
+ */
+
+ // GetValue is implemented natively by StringMap
+
+ /**
+ * Retrieves the value stored at an index.
+ *
+ * @param index Index to retrieve value for.
+ * @param value Variable to store value.
+ * @returns Value stored at index.
+ */
+ public bool GetValueIndexed(int index, any &value)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.GetValue(key, value);
+ }
+
+ // GetString is implemented natively by StringMap
+
+ /**
+ * Retrieves the string stored at an index.
+ *
+ * @param index Index to retrieve string value for.
+ * @param value String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ * @returns True on success. False if the key is not set, or the key is set as a value or array (not a string).
+ */
+ public bool GetStringIndexed(int index, char[] value, int max_size)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.GetString(key, value, max_size);
+ }
+
+ /**
+ * Retrieves the int stored at a key.
+ *
+ * @param key Key to retrieve int value for.
+ * @returns Value stored at key.
+ */
+ public int GetInt(const char[] key)
+ {
+ int value;
+ return (this.GetValue(key, value)) ? value : -1;
+ }
+
+ /**
+ * Retrieves the int stored at an index.
+ *
+ * @param index Index to retrieve int value for.
+ * @returns Value stored at index.
+ */
+ public int GetIntIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return -1;
+ }
+
+ return this.GetInt(key);
+ }
+
+ /**
+ * Retrieves the float stored at a key.
+ *
+ * @param key Key to retrieve float value for.
+ * @returns Value stored at key.
+ */
+ public float GetFloat(const char[] key)
+ {
+ float value;
+ return (this.GetValue(key, value)) ? value : -1.0;
+ }
+
+ /**
+ * Retrieves the float stored at an index.
+ *
+ * @param index Index to retrieve float value for.
+ * @returns Value stored at index.
+ */
+ public float GetFloatIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return -1.0;
+ }
+
+ return this.GetFloat(key);
+ }
+
+ /**
+ * Retrieves the bool stored at a key.
+ *
+ * @param key Key to retrieve bool value for.
+ * @returns Value stored at key.
+ */
+ public bool GetBool(const char[] key)
+ {
+ bool value;
+ return (this.GetValue(key, value)) ? value : false;
+ }
+
+ /**
+ * Retrieves the bool stored at an index.
+ *
+ * @param index Index to retrieve bool value for.
+ * @returns Value stored at index.
+ */
+ public bool GetBoolIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.GetBool(key);
+ }
+
+ /**
+ * Retrieves the handle stored at a key.
+ *
+ * @param key Key to retrieve handle value for.
+ * @returns Value stored at key.
+ */
+ public Handle GetHandle(const char[] key)
+ {
+ Handle value;
+ return (this.GetValue(key, value)) ? value : null;
+ }
+
+ /**
+ * Retrieves the handle stored at an index.
+ *
+ * @param index Index to retrieve handle value for.
+ * @returns Value stored at index.
+ */
+ public Handle GetHandleIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return null;
+ }
+
+ return this.GetHandle(key);
+ }
+
+ /**
+ * Retrieves the JSON object stored at a key.
+ *
+ * @param key Key to retrieve object value for.
+ * @returns Value stored at key.
+ */
+ public JSON_Object GetObject(const char[] key)
+ {
+ return view_as<JSON_Object>(this.GetHandle(key));
+ }
+
+ /**
+ * Retrieves the object stored at an index.
+ *
+ * @param index Index to retrieve object value for.
+ * @returns Value stored at index.
+ */
+ public JSON_Object GetObjectIndexed(int index)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return null;
+ }
+
+ return this.GetObject(key);
+ }
+
+ /**
+ * @section Setters
+ */
+
+ /**
+ * Sets the string stored at a key.
+ *
+ * @param key Key to set to string value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetString(const char[] key, const char[] value, bool replace = true)
+ {
+ return this.SetString(key, value, replace)
+ && this.SetKeyType(key, Type_String)
+ && this.SetKeyLength(key, strlen(value));
+ }
+
+ /**
+ * Sets the string stored at an index.
+ *
+ * @param index Index to set to string value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetStringIndexed(int index, const char[] value, bool replace = true)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetString(key, value, replace);
+ }
+
+ /**
+ * Sets the int stored at a key.
+ *
+ * @param key Key to set to int value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetInt(const char[] key, int value, bool replace = true)
+ {
+ return this.SetValue(key, value, replace)
+ && this.SetKeyType(key, Type_Int);
+ }
+
+ /**
+ * Sets the int stored at an index.
+ *
+ * @param index Index to set to int value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetIntIndexed(int index, int value, bool replace = true)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetInt(key, value, replace);
+ }
+
+ /**
+ * Sets the float stored at a key.
+ *
+ * @param key Key to set to float value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetFloat(const char[] key, float value, bool replace = true)
+ {
+ return this.SetValue(key, value, replace)
+ && this.SetKeyType(key, Type_Float);
+ }
+
+ /**
+ * Sets the float stored at an index.
+ *
+ * @param index Index to set to float value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetFloatIndexed(int index, float value, bool replace = true)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetFloat(key, value, replace);
+ }
+
+ /**
+ * Sets the bool stored at a key.
+ *
+ * @param key Key to set to bool value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetBool(const char[] key, bool value, bool replace = true)
+ {
+ return this.SetValue(key, value, replace)
+ && this.SetKeyType(key, Type_Bool);
+ }
+
+ /**
+ * Sets the bool stored at an index.
+ *
+ * @param index Index to set to bool value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetBoolIndexed(int index, bool value, bool replace = true)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetBool(key, value, replace);
+ }
+
+ /**
+ * Sets the handle stored at a key.
+ *
+ * @param key Key to set to handle value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetHandle(const char[] key, Handle value = null, bool replace = true)
+ {
+ return this.SetValue(key, value, replace)
+ && this.SetKeyType(key, Type_Null);
+ }
+
+ /**
+ * Sets the handle stored at an index.
+ *
+ * @param index Index to set to handle value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetHandleIndexed(int index, Handle value = null, bool replace = true)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetHandle(key, value, replace);
+ }
+
+ /**
+ * Sets the object stored at a key.
+ *
+ * @param key Key to set to object value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetObject(const char[] key, JSON_Object value, bool replace = true)
+ {
+ return this.SetValue(key, value, replace)
+ && this.SetKeyType(key, Type_Object);
+ }
+
+ /**
+ * Sets the object stored at an index.
+ *
+ * @param index Index to set to object value.
+ * @param value Value to set.
+ * @returns True on success, false otherwise.
+ */
+ public bool SetObjectIndexed(int index, JSON_Object value, bool replace = true)
+ {
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ return this.SetObject(key, value, replace);
+ }
+
+ /**
+ * @section Array setters.
+ */
+
+ /**
+ * Pushes a string to the end of the array.
+ *
+ * @param value Value to push.
+ * @returns True on success, false otherwise.
+ */
+ public bool PushString(const char[] value)
+ {
+ return this.SetStringIndexed(this.CurrentIndex, value)
+ && this.IncrementIndex();
+ }
+
+ /**
+ * Pushes an int to the end of the array.
+ *
+ * @param value Value to push.
+ * @returns True on success, false otherwise.
+ */
+ public bool PushInt(int value)
+ {
+ return this.SetIntIndexed(this.CurrentIndex, value)
+ && this.IncrementIndex();
+ }
+
+ /**
+ * Pushes a float to the end of the array.
+ *
+ * @param value Value to push.
+ * @returns True on success, false otherwise.
+ */
+ public bool PushFloat(float value)
+ {
+ return this.SetFloatIndexed(this.CurrentIndex, value)
+ && this.IncrementIndex();
+ }
+
+ /**
+ * Pushes a bool to the end of the array.
+ *
+ * @param value Value to push.
+ * @returns True on success, false otherwise.
+ */
+ public bool PushBool(bool value)
+ {
+ return this.SetBoolIndexed(this.CurrentIndex, value)
+ && this.IncrementIndex();
+ }
+
+ /**
+ * Pushes a handle to the end of the array.
+ *
+ * @param value Value to push.
+ * @returns True on success, false otherwise.
+ */
+ public bool PushHandle(Handle value = null)
+ {
+ return this.SetHandleIndexed(this.CurrentIndex, value)
+ && this.IncrementIndex();
+ }
+
+ /**
+ * Pushes an object to the end of the array.
+ *
+ * @param value Value to push.
+ * @returns True on success, false otherwise.
+ */
+ public bool PushObject(JSON_Object value)
+ {
+ return this.SetObjectIndexed(this.CurrentIndex, value)
+ && this.IncrementIndex();
+ }
+
+ /**
+ * @section Generic.
+ */
+
+ /**
+ * Finds the index of a value in the array.
+ *
+ * @param value Value to search for.
+ * @returns The index of the value if it is found, -1 otherwise.
+ */
+ public int IndexOf(any value)
+ {
+ any current;
+ for (int i = 0; i < this.CurrentIndex; ++i) {
+ if (this.GetValueIndexed(i, current) && value == current) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Finds the index of a string in the array.
+ *
+ * @param value String to search for.
+ * @returns The index of the string if it is found, -1 otherwise.
+ */
+ public int IndexOfString(const char[] value)
+ {
+ for (int i = 0; i < this.CurrentIndex; ++i) {
+ if (this.GetKeyTypeIndexed(i) != Type_String) {
+ continue;
+ }
+
+ int current_size = this.GetKeyLengthIndexed(i) + 1;
+ char[] current = new char[current_size];
+ this.GetStringIndexed(i, current, current_size);
+ if (StrEqual(value, current)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Determines whether the array contains a value.
+ *
+ * @param value Value to search for.
+ * @returns True if the value is found, false otherwise.
+ */
+ public bool Contains(any value)
+ {
+ return this.IndexOf(value) != -1;
+ }
+
+ /**
+ * Determines whether the array contains a string.
+ *
+ * @param value String to search for.
+ * @returns True if the string is found, false otherwise.
+ */
+ public bool ContainsString(const char[] value)
+ {
+ return this.IndexOfString(value) != -1;
+ }
+
+ /**
+ * Removes an item from the object by key.
+ *
+ * @param key Key of object to remove.
+ * @returns True on success, false if the value was never set.
+ */
+ public bool Remove(const char[] key) {
+ static char meta_keys[][] = {
+ JSON_META_TYPE_KEY, JSON_META_LENGTH_KEY, JSON_META_HIDDEN_KEY
+ };
+
+ // create a new char[] which will fit the longest meta-key
+ int meta_key_size = strlen(key) + 8;
+ char[] meta_key = new char[meta_key_size];
+
+ // view ourselves as a StringMap so we can call underlying Remove() method
+ StringMap self = view_as<StringMap>(this);
+
+ bool success = true;
+ for (int i = 0; i < sizeof(meta_keys); ++i) {
+ Format(meta_key, meta_key_size, "%s%s", key, meta_keys[i]);
+
+ if (this.HasKey(meta_key)) {
+ success = success && self.Remove(meta_key);
+ }
+ }
+
+ return success && self.Remove(key);
+ }
+
+ /**
+ * Removes a key and its related meta-keys from the object.
+ *
+ * @param key Key to remove.
+ * @returns True on success, false if the value was never set.
+ */
+ public bool RemoveIndexed(int index)
+ {
+ if (! this.HasIndex(index)) {
+ return false;
+ }
+
+ char key[JSON_INDEX_BUFFER_SIZE];
+ if (! this.GetIndexString(key, sizeof(key), index)) {
+ return false;
+ }
+
+ if (! this.Remove(key)) {
+ return false;
+ }
+
+ for (int i = index + 1; i < this.CurrentIndex; ++i) {
+ if (! this.GetIndexString(key, sizeof(key), i)) {
+ return false;
+ }
+
+ int target = i - 1;
+
+ JSON_CELL_TYPE type = this.GetKeyTypeIndexed(i);
+
+ switch (type) {
+ case Type_String: {
+ int str_length = this.GetKeyLengthIndexed(i);
+ char[] str_value = new char[str_length];
+
+ this.GetStringIndexed(i, str_value, str_length + 1);
+ this.SetStringIndexed(target, str_value);
+ }
+ case Type_Int: {
+ this.SetIntIndexed(target, this.GetIntIndexed(i));
+ }
+ case Type_Float: {
+ this.SetFloatIndexed(target, this.GetFloatIndexed(i));
+ }
+ case Type_Bool: {
+ this.SetBoolIndexed(target, this.GetBoolIndexed(i));
+ }
+ case Type_Null: {
+ this.SetHandleIndexed(target, this.GetHandleIndexed(i));
+ }
+ case Type_Object: {
+ this.SetObjectIndexed(target, this.GetObjectIndexed(i));
+ }
+ }
+
+ if (this.GetKeyHiddenIndexed(i)) {
+ this.SetKeyHiddenIndexed(target, true);
+ }
+
+ this.Remove(key);
+ }
+
+ this.CurrentIndex -= 1;
+
+ return true;
+ }
+
+ /**
+ * Encodes the instance into its string representation.
+ *
+ * @param output String buffer to store output.
+ * @param max_size Maximum size of string buffer.
+ * @param pretty_print Should the output be pretty printed (newlines and spaces)? [default: false]
+ * @param depth The current depth of the encoder. [default: 0]
+ */
+ public void Encode(char[] output, int max_size, bool pretty_print = false, int depth = 0)
+ {
+ json_encode(this, output, max_size, pretty_print, depth);
+ }
+
+ /**
+ * Decodes a JSON string into this object.
+ *
+ * @param buffer Buffer to decode.
+ */
+ public void Decode(const char[] buffer)
+ {
+ json_decode(buffer, this);
+ }
+
+ /**
+ * Recursively cleans up the object and any objects referenced within.
+ */
+ public void Cleanup()
+ {
+ json_cleanup(this);
+ }
+};
diff --git a/sourcemod/scripting/include/json/string_helpers.inc b/sourcemod/scripting/include/json/string_helpers.inc
new file mode 100644
index 0000000..6063d47
--- /dev/null
+++ b/sourcemod/scripting/include/json/string_helpers.inc
@@ -0,0 +1,77 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * sm-json
+ * Provides a pure SourcePawn implementation of JSON encoding and decoding.
+ * https://github.com/clugg/sm-json
+ *
+ * sm-json (C)2018 James D. (clug)
+ * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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," the "SourcePawn JIT," 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. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or <http://www.sourcemod.net/license.php>.
+ */
+
+#if defined _json_string_helpers_included
+ #endinput
+#endif
+#define _json_string_helpers_included
+
+/**
+ * Checks if a string starts with another string.
+ *
+ * @param haystack String to check that starts with needle.
+ * @param maxlen Maximum size of string buffer.
+ * @param needle String to check that haystack starts with.
+ * @return True if haystack begins with needle, false otherwise.
+ */
+stock bool json_string_startswith(const char[] haystack, const char[] needle) {
+ int haystack_length = strlen(haystack);
+ int needle_length = strlen(needle);
+ if (needle_length > haystack_length) return false;
+
+ for (int i = 0; i < needle_length; ++i) {
+ if (haystack[i] != needle[i]) return false;
+ }
+
+ return true;
+}
+
+/**
+ * Checks if a string ends with another string.
+ *
+ * @param haystack String to check that ends with needle.
+ * @param maxlen Maximum size of string buffer.
+ * @param needle String to check that haystack ends with.
+ * @return True if haystack ends with needle, false otherwise.
+ */
+stock bool json_string_endswith(const char[] haystack, const char[] needle) {
+ int haystack_length = strlen(haystack);
+ int needle_length = strlen(needle);
+ if (needle_length > haystack_length) return false;
+
+ for (int i = 0; i < needle_length; ++i) {
+ if (haystack[haystack_length - needle_length + i] != needle[i]) return false;
+ }
+
+ return true;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/movement.inc b/sourcemod/scripting/include/movement.inc
new file mode 100644
index 0000000..7cc5b29
--- /dev/null
+++ b/sourcemod/scripting/include/movement.inc
@@ -0,0 +1,530 @@
+/*
+ MovementAPI Function Stock Library
+
+ Website: https://github.com/danzayau/MovementAPI
+*/
+
+#if defined _movement_included_
+ #endinput
+#endif
+#define _movement_included_
+
+#include <sdktools>
+
+
+
+// =====[ STOCKS ]=====
+
+/**
+ * Calculates the horizontal distance between two vectors.
+ *
+ * @param vec1 First vector.
+ * @param vec2 Second vector.
+ * @return Vector horizontal distance.
+ */
+stock float GetVectorHorizontalDistance(const float vec1[3], const float vec2[3])
+{
+ return SquareRoot(Pow(vec2[0] - vec1[0], 2.0) + Pow(vec2[1] - vec1[1], 2.0));
+}
+
+/**
+ * Calculates a vector's horizontal length.
+ *
+ * @param vec Vector.
+ * @return Vector horizontal length (magnitude).
+ */
+stock float GetVectorHorizontalLength(const float vec[3])
+{
+ return SquareRoot(Pow(vec[0], 2.0) + Pow(vec[1], 2.0));
+}
+
+/**
+ * Scales a vector to a certain horizontal length.
+ *
+ * @param vec Vector.
+ * @param length New horizontal length.
+ */
+stock void SetVectorHorizontalLength(float vec[3], float length)
+{
+ float newVec[3];
+ newVec = vec;
+ newVec[2] = 0.0;
+ NormalizeVector(newVec, newVec);
+ ScaleVector(newVec, length);
+ newVec[2] = vec[2];
+ vec = newVec;
+}
+
+/**
+ * Gets a player's currently pressed buttons.
+ *
+ * @param client Client index.
+ * @return Bitsum of buttons.
+ */
+stock int Movement_GetButtons(int client)
+{
+ return GetClientButtons(client);
+}
+
+/**
+ * Gets a player's origin.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void Movement_GetOrigin(int client, float result[3])
+{
+ GetClientAbsOrigin(client, result);
+}
+
+/**
+ * Gets a player's origin.
+ * If the player is on the ground, a trace hull is used to find the
+ * exact height of the ground the player is standing on. This is thus
+ * more accurate than Movement_GetOrigin when player is on ground.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void Movement_GetOriginEx(int client, float result[3])
+{
+ if (!Movement_GetOnGround(client))
+ {
+ GetClientAbsOrigin(client, result);
+ return;
+ }
+
+ // Get the coordinate of the solid beneath the player's origin
+ // More accurate than GetClientAbsOrigin when on ground
+ float startPosition[3], endPosition[3];
+ GetClientAbsOrigin(client, startPosition);
+ endPosition = startPosition;
+ endPosition[2] = startPosition[2] - 2.0; // Should be less than 2.0 units away
+ Handle trace = TR_TraceHullFilterEx(
+ startPosition,
+ endPosition,
+ view_as<float>( { -16.0, -16.0, 0.0 } ), // Players are 32 x 32 x 72
+ view_as<float>( { 16.0, 16.0, 72.0 } ),
+ MASK_PLAYERSOLID,
+ TraceEntityFilterPlayers,
+ client);
+ if (TR_DidHit(trace))
+ {
+ TR_GetEndPosition(result, trace);
+ // Do not get rid of the offset. The offset is correct, as the player must be
+ // at least 0.03125 units away from the ground.
+ }
+ else
+ {
+ result = startPosition; // Fallback to GetClientAbsOrigin
+ }
+ delete trace;
+}
+
+public bool TraceEntityFilterPlayers(int entity, int contentsMask)
+{
+ return entity > MaxClients;
+}
+
+/**
+ * Sets a player's origin by teleporting them.
+ *
+ * @param client Client index.
+ * @param origin New origin.
+ */
+stock void Movement_SetOrigin(int client, const float origin[3])
+{
+ TeleportEntity(client, origin, NULL_VECTOR, NULL_VECTOR);
+}
+
+/**
+ * Gets a player's velocity.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void Movement_GetVelocity(int client, float result[3])
+{
+ GetEntPropVector(client, Prop_Data, "m_vecVelocity", result);
+}
+
+/**
+ * Sets a player's velocity by teleporting them.
+ *
+ * @param client Client index.
+ * @param velocity New velocity.
+ */
+stock void Movement_SetVelocity(int client, const float velocity[3])
+{
+ TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, velocity);
+}
+
+/**
+ * Gets a player's horizontal speed.
+ *
+ * @param client Client index.
+ * @return Player's horizontal speed.
+ */
+stock float Movement_GetSpeed(int client)
+{
+ float velocity[3];
+ Movement_GetVelocity(client, velocity);
+ return GetVectorHorizontalLength(velocity);
+}
+
+/**
+ * Sets a player's horizontal speed.
+ *
+ * @param client Client index.
+ * @param value New horizontal speed.
+ * @param applyBaseVel Whether to apply base velocity as well.
+ */
+stock void Movement_SetSpeed(int client, float value, bool applyBaseVel = false)
+{
+ float velocity[3];
+ Movement_GetVelocity(client, velocity);
+ SetVectorHorizontalLength(velocity, value)
+ if (applyBaseVel)
+ {
+ float baseVelocity[3];
+ Movement_GetBaseVelocity(client, baseVelocity);
+ AddVectors(velocity, baseVelocity, velocity);
+ }
+ Movement_SetVelocity(client, velocity);
+}
+
+/**
+ * Gets a player's vertical velocity.
+ *
+ * @param client Client index.
+ * @return Player's vertical velocity.
+ */
+stock float Movement_GetVerticalVelocity(int client)
+{
+ float velocity[3];
+ Movement_GetVelocity(client, velocity);
+ return velocity[2];
+}
+
+/**
+ * Sets a player's vertical velocity.
+ *
+ * @param client Client index.
+ * @param value New vertical velocity.
+ */
+stock void Movement_SetVerticalVelocity(int client, float value)
+{
+ float velocity[3];
+ Movement_GetVelocity(client, velocity);
+ velocity[2] = value;
+ Movement_SetVelocity(client, velocity);
+}
+
+/**
+ * Gets a player's base velocity.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void Movement_GetBaseVelocity(int client, float result[3])
+{
+ GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", result);
+}
+
+/**
+ * Sets a player's base velocity.
+ *
+ * @param client Client index.
+ * @param baseVelocity New base velocity.
+ */
+stock void Movement_SetBaseVelocity(int client, const float baseVelocity[3])
+{
+ SetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity);
+}
+
+/**
+ * Gets a player's eye angles.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+stock void Movement_GetEyeAngles(int client, float result[3])
+{
+ GetClientEyeAngles(client, result);
+}
+
+/**
+ * Sets a player's eye angles by teleporting them.
+ *
+ * @param client Client index.
+ * @param eyeAngles New eye angles.
+ */
+stock void Movement_SetEyeAngles(int client, const float eyeAngles[3])
+{
+ TeleportEntity(client, NULL_VECTOR, eyeAngles, NULL_VECTOR);
+}
+
+/**
+ * Gets whether a player is on the ground.
+ *
+ * @param client Client index.
+ * @return Whether player is on the ground.
+ */
+stock bool Movement_GetOnGround(int client)
+{
+ return view_as<bool>(GetEntityFlags(client) & FL_ONGROUND);
+}
+
+/**
+ * Gets whether a player is ducking or ducked.
+ *
+ * @param client Client index.
+ * @return Whether player is ducking or ducked.
+ */
+stock bool Movement_GetDucking(int client)
+{
+ return GetEntProp(client, Prop_Send, "m_bDucked") || GetEntProp(client, Prop_Send, "m_bDucking");
+}
+
+/**
+ * Gets a player's "m_flDuckSpeed" value.
+ *
+ * @param client Client index.
+ * @return Value of "m_flDuckSpeed".
+ */
+stock float Movement_GetDuckSpeed(int client)
+{
+ return GetEntPropFloat(client, Prop_Send, "m_flDuckSpeed");
+}
+
+/**
+ * Sets a player's "m_flDuckSpeed" value.
+ *
+ * @param client Client index.
+ * @param value New "m_flDuckSpeed" value.
+ */
+stock void Movement_SetDuckSpeed(int client, float value)
+{
+ SetEntPropFloat(client, Prop_Send, "m_flDuckSpeed", value);
+}
+
+/**
+ * Gets a player's "m_flVelocityModifier" value.
+ *
+ * @param client Client index.
+ * @return Value of "m_flVelocityModifier".
+ */
+stock float Movement_GetVelocityModifier(int client)
+{
+ return GetEntPropFloat(client, Prop_Send, "m_flVelocityModifier");
+}
+
+/**
+ * Sets a player's "m_flVelocityModifier" value.
+ *
+ * @param client Client index.
+ * @param value New "m_flVelocityModifier" value.
+ */
+stock void Movement_SetVelocityModifier(int client, float value)
+{
+ SetEntPropFloat(client, Prop_Send, "m_flVelocityModifier", value);
+}
+
+/**
+ * Gets a player's gravity scale factor.
+ *
+ * @param client Client index.
+ * @return Gravity scale factor.
+ */
+stock float Movement_GetGravity(int client)
+{
+ return GetEntityGravity(client);
+}
+
+/**
+ * Sets a player's gravity scale factor.
+ *
+ * @param client Client index.
+ * @param value Desired gravity scale factor.
+ */
+stock void Movement_SetGravity(int client, float value)
+{
+ SetEntityGravity(client, value);
+}
+
+/**
+ * Gets a player's movetype.
+ *
+ * @param client Client index.
+ * @return Player's movetype.
+ */
+stock MoveType Movement_GetMovetype(int client)
+{
+ return GetEntityMoveType(client);
+}
+
+/**
+ * Sets a player's movetype.
+ *
+ * @param client Client index.
+ * @param movetype New movetype.
+ */
+stock void Movement_SetMovetype(int client, MoveType movetype)
+{
+ SetEntityMoveType(client, movetype);
+}
+
+/**
+ * Gets whether a player is on a ladder.
+ *
+ * @param client Client index.
+ * @return Whether player is on a ladder.
+ */
+stock bool Movement_GetOnLadder(int client)
+{
+ return GetEntityMoveType(client) == MOVETYPE_LADDER;
+}
+
+/**
+ * Gets whether a player is noclipping.
+ *
+ * @param client Client index.
+ * @return Whether player is noclipping.
+ */
+stock bool Movement_GetNoclipping(int client)
+{
+ return GetEntityMoveType(client) == MOVETYPE_NOCLIP;
+}
+
+
+
+// =====[ METHODMAP ]=====
+
+methodmap MovementPlayer {
+
+ public MovementPlayer(int client) {
+ return view_as<MovementPlayer>(client);
+ }
+
+ property int ID {
+ public get() {
+ return view_as<int>(this);
+ }
+ }
+
+ property int Buttons {
+ public get() {
+ return Movement_GetButtons(this.ID);
+ }
+ }
+
+ public void GetOrigin(float result[3]) {
+ Movement_GetOrigin(this.ID, result);
+ }
+
+ public void SetOrigin(const float origin[3]) {
+ Movement_SetOrigin(this.ID, origin);
+ }
+
+ public void GetVelocity(float result[3]) {
+ Movement_GetVelocity(this.ID, result);
+ }
+
+ public void SetVelocity(const float velocity[3]) {
+ Movement_SetVelocity(this.ID, velocity);
+ }
+
+ property float Speed {
+ public get() {
+ return Movement_GetSpeed(this.ID);
+ }
+ public set(float value) {
+ Movement_SetSpeed(this.ID, value);
+ }
+ }
+
+ property float VerticalVelocity {
+ public get() {
+ return Movement_GetVerticalVelocity(this.ID);
+ }
+ public set(float value) {
+ Movement_SetVerticalVelocity(this.ID, value);
+ }
+ }
+
+ public void GetBaseVelocity(float result[3]) {
+ Movement_GetBaseVelocity(this.ID, result);
+ }
+
+ public void SetBaseVelocity(const float baseVelocity[3]) {
+ Movement_SetBaseVelocity(this.ID, baseVelocity);
+ }
+
+ public void GetEyeAngles(float result[3]) {
+ Movement_GetEyeAngles(this.ID, result);
+ }
+
+ public void SetEyeAngles(const float eyeAngles[3]) {
+ Movement_SetEyeAngles(this.ID, eyeAngles);
+ }
+
+ property bool OnGround {
+ public get() {
+ return Movement_GetOnGround(this.ID);
+ }
+ }
+
+ property bool Ducking {
+ public get() {
+ return Movement_GetDucking(this.ID);
+ }
+ }
+
+ property float DuckSpeed {
+ public get() {
+ return Movement_GetDuckSpeed(this.ID);
+ }
+ public set(float value) {
+ Movement_SetDuckSpeed(this.ID, value);
+ }
+ }
+
+ property float VelocityModifier {
+ public get() {
+ return Movement_GetVelocityModifier(this.ID);
+ }
+ public set(float value) {
+ Movement_SetVelocityModifier(this.ID, value);
+ }
+ }
+
+ property float Gravity {
+ public get() {
+ return Movement_GetGravity(this.ID);
+ }
+ public set(float value) {
+ Movement_SetGravity(this.ID, value);
+ }
+ }
+
+ property MoveType Movetype {
+ public get() {
+ return Movement_GetMovetype(this.ID);
+ }
+ public set(MoveType movetype) {
+ Movement_SetMovetype(this.ID, movetype);
+ }
+ }
+
+ property bool OnLadder {
+ public get() {
+ return Movement_GetOnLadder(this.ID);
+ }
+ }
+
+ property bool Noclipping {
+ public get() {
+ return Movement_GetNoclipping(this.ID);
+ }
+ }
+}
diff --git a/sourcemod/scripting/include/movementapi.inc b/sourcemod/scripting/include/movementapi.inc
new file mode 100644
index 0000000..290c3f2
--- /dev/null
+++ b/sourcemod/scripting/include/movementapi.inc
@@ -0,0 +1,663 @@
+/*
+ MovementAPI Plugin Include
+
+ Website: https://github.com/danzayau/MovementAPI
+*/
+
+#if defined _movementapi_included_
+ #endinput
+#endif
+#define _movementapi_included_
+
+#include <movement>
+
+
+
+/*
+ Terminology
+
+ Takeoff
+ Becoming airborne, including jumping, falling, getting off a ladder and leaving noclip.
+
+ Landing
+ Leaving the air, including landing on the ground, grabbing a ladder and entering noclip.
+
+ Perfect Bunnyhop (Perf)
+ When the player has jumped in the tick after landing and keeps their speed.
+
+ Duckbug/Crouchbug
+ When the player sucessfully lands due to uncrouching from mid air and not by falling
+ down. This causes no stamina loss or fall damage upon landing.
+
+ Jumpbug
+ This is achieved by duckbugging and jumping at the same time. The player is never seen
+ as 'on ground' when bunnyhopping from a tick by tick perspective. A jumpbug inherits
+ the same behavior as a duckbug/crouchbug, along with its effects such as maintaining
+ speed due to no stamina loss.
+
+ Distbug
+ Landing behavior varies depending on whether the player lands close to the edge of a
+ block or not:
+
+ 1. If the player lands close to the edge of a block, this causes the jump duration to
+ be one tick longer and the player can "slide" on the ground during the landing tick,
+ using the position post-tick as landing position becomes inaccurate.
+
+ 2. On the other hand, if the player does not land close to the edge, the player will
+ be considered on the ground one tick earlier, using this position as landing position
+ is not accurate as the player has yet to be fully on the ground.
+
+ In scenario 1, GetNobugLandingOrigin calculates the correct landing position of the
+ player before the sliding effect takes effect.
+
+ In scenario 2, GetNobugLandingOrigin attempts to extrapolate the player's fully on
+ ground position to make landing positions consistent across scenarios.
+*/
+
+
+
+// =====[ FORWARDS ]=====
+
+/**
+ * Called when a player's movetype changes.
+ *
+ * @param client Client index.
+ * @param oldMovetype Player's old movetype.
+ * @param newMovetype Player's new movetype.
+ */
+forward void Movement_OnChangeMovetype(int client, MoveType oldMovetype, MoveType newMovetype);
+
+/**
+ * Called when a player touches the ground.
+ *
+ * @param client Client index.
+ */
+forward void Movement_OnStartTouchGround(int client);
+
+/**
+ * Called when a player leaves the ground.
+ *
+ * @param client Client index.
+ * @param jumped Whether player jumped to leave ground.
+ * @param ladderJump Whether player jumped from a ladder.
+ * @param jumpbug Whether player performed a jumpbug.
+ */
+forward void Movement_OnStopTouchGround(int client, bool jumped, bool ladderJump, bool jumpbug);
+
+/**
+ * Called when a player starts ducking.
+ *
+ * @param client Client index.
+ */
+forward void Movement_OnStartDucking(int client);
+
+/**
+ * Called when a player stops ducking.
+ *
+ * @param client Client index.
+ */
+forward void Movement_OnStopDucking(int client);
+
+/**
+ * Called when a player jumps (player_jump event), including 'jumpbugs'.
+ * Setting velocity when this is called may not be effective.
+ *
+ * @param client Client index.
+ * @param jumpbug Whether player 'jumpbugged'.
+ */
+forward void Movement_OnPlayerJump(int client, bool jumpbug);
+
+/**
+ * Called before PlayerMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnPlayerMovePre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after PlayerMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnPlayerMovePost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called before Duck movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnDuckPre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after Duck movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnDuckPost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called before LadderMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnLadderMovePre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after LadderMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnLadderMovePost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called before FullLadderMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnFullLadderMovePre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after FullLadderMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnFullLadderMovePost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after the player jumps, but before jumping stamina is applied and takeoff variables are not set yet.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnJumpPre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after the player jumps and after jumping stamina is applied and takeoff variables are already set here.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnJumpPost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called before AirAccelerate movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnAirAcceleratePre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after AirAccelerate movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnAirAcceleratePost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called before WalkMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnWalkMovePre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after WalkMove movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnWalkMovePost(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called before CategorizePosition movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnCategorizePositionPre(int client, float origin[3], float velocity[3]);
+
+/**
+ * Called after CategorizePosition movement function is called.
+ * Modifying origin or velocity parameters will change player's origin and velocity accordingly.
+ *
+ * @param client Client index.
+ * @param origin Player origin.
+ * @param velocity Player velocity.
+ * @return Plugin_Changed if origin or velocity is changed, Plugin_Continue otherwise.
+ */
+forward Action Movement_OnCategorizePositionPost(int client, float origin[3], float velocity[3]);
+
+// =====[ NATIVES ]=====
+
+/**
+ * Gets whether a player's last takeoff was a jump.
+ *
+ * @param client Client index.
+ * @return Whether player's last takeoff was a jump.
+ */
+native bool Movement_GetJumped(int client);
+
+/**
+ * Gets whether a player's last takeoff was a perfect bunnyhop.
+ *
+ * @param client Client index.
+ * @return Whether player's last takeoff was a perfect bunnyhop.
+ */
+native bool Movement_GetHitPerf(int client);
+
+/**
+ * Gets a player's origin at the time of their last takeoff.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+native void Movement_GetTakeoffOrigin(int client, float result[3]);
+
+/**
+ * Gets a player's velocity at the time of their last takeoff.
+ *
+ * If sv_enablebunnyhopping is 0, CS:GO may adjust the player's
+ * velocity after the takeoff velocity has already been measured.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+native void Movement_GetTakeoffVelocity(int client, float result[3]);
+
+/**
+ * Gets a player's horizontal speed at the time of their last takeoff.
+ *
+ * If sv_enablebunnyhopping is 0, CS:GO may adjust the player's
+ * velocity after the takeoff velocity has already been measured.
+ *
+ * @param client Client index.
+ * @return Player's last takeoff speed.
+ */
+native float Movement_GetTakeoffSpeed(int client);
+
+/**
+ * Gets a player's 'tickcount' at the time of their last takeoff.
+ *
+ * @param client Client index.
+ * @return Player's last takeoff 'tickcount'.
+ */
+native int Movement_GetTakeoffTick(int client);
+
+/**
+ * Gets a player's 'cmdnum' at the time of their last takeoff.
+ *
+ * @param client Client index.
+ * @return Player's last takeoff 'cmdnum'.
+ */
+native int Movement_GetTakeoffCmdNum(int client);
+
+/**
+ * Gets a player's origin at the time of their last landing with the distbug fixed.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+native void Movement_GetNobugLandingOrigin(int client, float result[3]);
+
+/**
+ * Gets a player's origin at the time of their last landing.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+native void Movement_GetLandingOrigin(int client, float result[3]);
+
+/**
+ * Gets a player's velocity at the time of their last landing.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+native void Movement_GetLandingVelocity(int client, float result[3]);
+
+/**
+ * Gets a player's horizontal speed at the time of their last landing.
+ *
+ * @param client Client index.
+ * @return Last landing speed of the player (horizontal).
+ */
+native float Movement_GetLandingSpeed(int client);
+
+/**
+ * Gets a player's 'tickcount' at the time of their last landing.
+ *
+ * @param client Client index.
+ * @return Player's last landing 'tickcount'.
+ */
+native int Movement_GetLandingTick(int client);
+
+/**
+ * Gets a player's 'cmdnum' at the time of their last landing.
+ *
+ * @param client Client index.
+ * @return Player's last landing 'cmdnum'.
+ */
+native int Movement_GetLandingCmdNum(int client);
+
+/**
+ * Gets whether a player is turning their aim horizontally.
+ *
+ * @param client Client index.
+ * @return Whether player is turning their aim horizontally.
+ */
+native bool Movement_GetTurning(int client);
+
+/**
+ * Gets whether a player is turning their aim left.
+ *
+ * @param client Client index.
+ * @return Whether player is turning their aim left.
+ */
+native bool Movement_GetTurningLeft(int client);
+
+/**
+ * Gets whether a player is turning their aim right.
+ *
+ * @param client Client index.
+ * @return Whether player is turning their aim right.
+ */
+native bool Movement_GetTurningRight(int client);
+
+/**
+ * Gets result of CCSPlayer::GetPlayerMaxSpeed(client), which
+ * is the player's max speed as limited by their weapon.
+ *
+ * @param client Client index.
+ * @return Player's max speed as limited by their weapon.
+ */
+native float Movement_GetMaxSpeed(int client);
+
+/**
+ * Gets whether a player duckbugged on this tick.
+ *
+ * @param client Client index.
+ * @return Whether a player duckbugged on this tick.
+ */
+native bool Movement_GetDuckbugged(int client);
+
+/**
+ * Gets whether a player jumpbugged on this tick.
+ *
+ * @param client Client index.
+ * @return Whether a player jumpbugged on this tick.
+ */
+native bool Movement_GetJumpbugged(int client);
+
+/**
+ * Get the player's origin during movement processing.
+ *
+ * @param client Client index.
+ * @param result Resultant vector.
+ */
+native void Movement_GetProcessingOrigin(int client, float result[3]);
+
+/**
+ * Get the player's velocity during movement processing.
+ *
+ * @param client Param description
+ * @param result Resultant vector.
+ */
+native void Movement_GetProcessingVelocity(int client, float result[3]);
+
+/**
+ * Set the player's takeoff origin.
+ *
+ * @param client Client index.
+ * @param origin Desired origin.
+ */
+native void Movement_SetTakeoffOrigin(int client, float origin[3]);
+
+/**
+ * Set the player's takeoff velocity.
+ *
+ * @param client Client index.
+ * @param origin Desired velocity.
+ */
+native void Movement_SetTakeoffVelocity(int client, float velocity[3]);
+
+/**
+ * Set the player's landing origin.
+ *
+ * @param client Client index.
+ * @param origin Desired origin.
+ */
+native void Movement_SetLandingOrigin(int client, float origin[3]);
+
+/**
+ * Set the player's landing velocity.
+ *
+ * @param client Client index.
+ * @param origin Desired velocity.
+ */
+native void Movement_SetLandingVelocity(int client, float velocity[3]);
+
+// =====[ METHODMAP ]=====
+
+methodmap MovementAPIPlayer < MovementPlayer {
+
+ public MovementAPIPlayer(int client) {
+ return view_as<MovementAPIPlayer>(MovementPlayer(client));
+ }
+
+ property bool Jumped {
+ public get() {
+ return Movement_GetJumped(this.ID);
+ }
+ }
+
+ property bool HitPerf {
+ public get() {
+ return Movement_GetHitPerf(this.ID);
+ }
+ }
+
+ public void GetTakeoffOrigin(float buffer[3]) {
+ Movement_GetTakeoffOrigin(this.ID, buffer);
+ }
+
+ public void GetTakeoffVelocity(float buffer[3]) {
+ Movement_GetTakeoffVelocity(this.ID, buffer);
+ }
+
+ public void SetTakeoffOrigin(float buffer[3])
+ {
+ Movement_SetTakeoffOrigin(this.ID, buffer);
+ }
+
+ public void SetTakeoffVelocity(float buffer[3])
+ {
+ Movement_SetTakeoffVelocity(this.ID, buffer);
+ }
+
+ property float TakeoffSpeed {
+ public get() {
+ return Movement_GetTakeoffSpeed(this.ID);
+ }
+ }
+
+ property int TakeoffTick {
+ public get() {
+ return Movement_GetTakeoffTick(this.ID);
+ }
+ }
+
+ property int TakeoffCmdNum {
+ public get() {
+ return Movement_GetTakeoffCmdNum(this.ID);
+ }
+ }
+
+ public void GetLandingOrigin(float buffer[3]) {
+ Movement_GetLandingOrigin(this.ID, buffer);
+ }
+
+ public void GetLandingVelocity(float buffer[3]) {
+ Movement_GetLandingVelocity(this.ID, buffer);
+ }
+
+ public void SetLandingOrigin(float buffer[3])
+ {
+ Movement_SetLandingOrigin(this.ID, buffer);
+ }
+
+ public void SetLandingVelocity(float buffer[3])
+ {
+ Movement_SetLandingVelocity(this.ID, buffer);
+ }
+
+ property float LandingSpeed {
+ public get() {
+ return Movement_GetLandingSpeed(this.ID);
+ }
+ }
+
+ property int LandingTick {
+ public get() {
+ return Movement_GetLandingTick(this.ID);
+ }
+ }
+
+ property int LandingCmdNum {
+ public get() {
+ return Movement_GetLandingCmdNum(this.ID);
+ }
+ }
+
+ property bool Turning {
+ public get() {
+ return Movement_GetTurning(this.ID);
+ }
+ }
+
+ property bool TurningLeft {
+ public get() {
+ return Movement_GetTurningLeft(this.ID);
+ }
+ }
+
+ property bool TurningRight {
+ public get() {
+ return Movement_GetTurningRight(this.ID);
+ }
+ }
+
+ property float MaxSpeed {
+ public get() {
+ return Movement_GetMaxSpeed(this.ID);
+ }
+ }
+
+ public void GetProcessingVelocity(float buffer[3])
+ {
+ Movement_GetProcessingVelocity(this.ID, buffer);
+ }
+
+ public void GetProcessingOrigin(float buffer[3])
+ {
+ Movement_GetProcessingOrigin(this.ID, buffer);
+ }
+}
+
+
+
+// =====[ DEPENDENCY ]=====
+
+public SharedPlugin __pl_movementapi =
+{
+ name = "movementapi",
+ file = "movementapi.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1,
+ #else
+ required = 0,
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_movementapi_SetNTVOptional()
+{
+ MarkNativeAsOptional("Movement_GetJumped");
+ MarkNativeAsOptional("Movement_GetHitPerf");
+ MarkNativeAsOptional("Movement_GetTakeoffOrigin");
+ MarkNativeAsOptional("Movement_GetTakeoffVelocity");
+ MarkNativeAsOptional("Movement_GetTakeoffSpeed");
+ MarkNativeAsOptional("Movement_GetTakeoffTick");
+ MarkNativeAsOptional("Movement_GetTakeoffCmdNum");
+ MarkNativeAsOptional("Movement_GetLandingOrigin");
+ MarkNativeAsOptional("Movement_GetLandingVelocity");
+ MarkNativeAsOptional("Movement_GetLandingSpeed");
+ MarkNativeAsOptional("Movement_GetLandingTick");
+ MarkNativeAsOptional("Movement_GetLandingCmdNum");
+ MarkNativeAsOptional("Movement_GetTurning");
+ MarkNativeAsOptional("Movement_GetTurningLeft");
+ MarkNativeAsOptional("Movement_GetTurningRight");
+ MarkNativeAsOptional("Movement_GetMaxSpeed");
+ MarkNativeAsOptional("Movement_GetProcessingOrigin");
+ MarkNativeAsOptional("Movement_GetProcessingVelocity");
+ MarkNativeAsOptional("Movement_SetTakeoffOrigin");
+ MarkNativeAsOptional("Movement_SetTakeoffVelocity");
+ MarkNativeAsOptional("Movement_SetLandingOrigin");
+ MarkNativeAsOptional("Movement_SetLandingVelocity");
+}
+#endif \ No newline at end of file
diff --git a/sourcemod/scripting/include/smjansson.inc b/sourcemod/scripting/include/smjansson.inc
new file mode 100644
index 0000000..029a492
--- /dev/null
+++ b/sourcemod/scripting/include/smjansson.inc
@@ -0,0 +1,1328 @@
+#if defined _jansson_included_
+ #endinput
+#endif
+#define _jansson_included_
+
+
+/**
+ * --- Type
+ *
+ * The JSON specification (RFC 4627) defines the following data types:
+ * object, array, string, number, boolean, and null.
+ * JSON types are used dynamically; arrays and objects can hold any
+ * other data type, including themselves. For this reason, Jansson�s
+ * type system is also dynamic in nature. There�s one Handle type to
+ * represent all JSON values, and the referenced structure knows the
+ * type of the JSON value it holds.
+ *
+ */
+enum json_type {
+ JSON_OBJECT,
+ JSON_ARRAY,
+ JSON_STRING,
+ JSON_INTEGER,
+ JSON_REAL,
+ JSON_TRUE,
+ JSON_FALSE,
+ JSON_NULL
+}
+
+/**
+ * Return the type of the JSON value.
+ *
+ * @param hObj Handle to the JSON value
+ *
+ * @return json_type of the value.
+ */
+native json_type:json_typeof(Handle:hObj);
+
+/**
+ * The type of a JSON value is queried and tested using these macros
+ *
+ * @param %1 Handle to the JSON value
+ *
+ * @return True if the value has the correct type.
+ */
+#define json_is_object(%1) ( json_typeof(%1) == JSON_OBJECT )
+#define json_is_array(%1) ( json_typeof(%1) == JSON_ARRAY )
+#define json_is_string(%1) ( json_typeof(%1) == JSON_STRING )
+#define json_is_integer(%1) ( json_typeof(%1) == JSON_INTEGER )
+#define json_is_real(%1) ( json_typeof(%1) == JSON_REAL )
+#define json_is_true(%1) ( json_typeof(%1) == JSON_TRUE )
+#define json_is_false(%1) ( json_typeof(%1) == JSON_FALSE )
+#define json_is_null(%1) ( json_typeof(%1) == JSON_NULL )
+#define json_is_number(%1) ( json_typeof(%1) == JSON_INTEGER || json_typeof(%1) == JSON_REAL )
+#define json_is_boolean(%1) ( json_typeof(%1) == JSON_TRUE || json_typeof(%1) == JSON_FALSE )
+
+/**
+ * Saves json_type as a String in output
+ *
+ * @param input json_type value to convert to string
+ * @param output Buffer to store the json_type value
+ * @param maxlength Maximum length of string buffer.
+ *
+ * @return False if the type does not exist.
+ */
+stock bool:Stringify_json_type(json_type:input, String:output[], maxlength) {
+ switch(input) {
+ case JSON_OBJECT: strcopy(output, maxlength, "Object");
+ case JSON_ARRAY: strcopy(output, maxlength, "Array");
+ case JSON_STRING: strcopy(output, maxlength, "String");
+ case JSON_INTEGER: strcopy(output, maxlength, "Integer");
+ case JSON_REAL: strcopy(output, maxlength, "Real");
+ case JSON_TRUE: strcopy(output, maxlength, "True");
+ case JSON_FALSE: strcopy(output, maxlength, "False");
+ case JSON_NULL: strcopy(output, maxlength, "Null");
+ default: return false;
+ }
+
+ return true;
+}
+
+
+
+/**
+ * --- Equality
+ *
+ * - Two integer or real values are equal if their contained numeric
+ * values are equal. An integer value is never equal to a real value,
+ * though.
+ * - Two strings are equal if their contained UTF-8 strings are equal,
+ * byte by byte. Unicode comparison algorithms are not implemented.
+ * - Two arrays are equal if they have the same number of elements and
+ * each element in the first array is equal to the corresponding
+ * element in the second array.
+ * - Two objects are equal if they have exactly the same keys and the
+ * value for each key in the first object is equal to the value of
+ * the corresponding key in the second object.
+ * - Two true, false or null values have no "contents", so they are
+ * equal if their types are equal.
+ *
+ */
+
+/**
+ * Test whether two JSON values are equal.
+ *
+ * @param hObj Handle to the first JSON object
+ * @param hOther Handle to the second JSON object
+ *
+ * @return Returns false if they are inequal or one
+ * or both of the pointers are NULL.
+ */
+native bool:json_equal(Handle:hObj, Handle:hOther);
+
+
+
+
+/**
+ * --- Copying
+ *
+ * Jansson supports two kinds of copying: shallow and deep. There is
+ * a difference between these methods only for arrays and objects.
+ *
+ * Shallow copying only copies the first level value (array or object)
+ * and uses the same child values in the copied value.
+ *
+ * Deep copying makes a fresh copy of the child values, too. Moreover,
+ * all the child values are deep copied in a recursive fashion.
+ *
+ */
+
+/**
+ * Get a shallow copy of the passed object
+ *
+ * @param hObj Handle to JSON object to be copied
+ *
+ * @return Returns a shallow copy of the object,
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_copy(Handle:hObj);
+
+/**
+ * Get a deep copy of the passed object
+ *
+ * @param hObj Handle to JSON object to be copied
+ *
+ * @return Returns a deep copy of the object,
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_deep_copy(Handle:hObj);
+
+
+
+
+/**
+ * --- Objects
+ *
+ * A JSON object is a dictionary of key-value pairs, where the
+ * key is a Unicode string and the value is any JSON value.
+ *
+ */
+
+/**
+ * Returns a handle to a new JSON object, or INVALID_HANDLE on error.
+ * Initially, the object is empty.
+ *
+ * @return Handle to a new JSON object.
+ */
+native Handle:json_object();
+
+/**
+ * Returns the number of elements in hObj
+ *
+ * @param hObj Handle to JSON object
+ *
+ * @return Number of elements in hObj,
+ * or 0 if hObj is not a JSON object.
+ */
+native json_object_size(Handle:hObj);
+
+/**
+ * Get a value corresponding to sKey from hObj
+ *
+ * @param hObj Handle to JSON object to get a value from
+ * @param sKey Key to retrieve
+ *
+ * @return Handle to a the JSON object or
+ * INVALID_HANDLE on error.
+ */
+native Handle:json_object_get(Handle:hObj, const String:sKey[]);
+
+/**
+ * Set the value of sKey to hValue in hObj.
+ * If there already is a value for key, it is replaced by the new value.
+ *
+ * @param hObj Handle to JSON object to set a value on
+ * @param sKey Key to store in the object
+ * Must be a valid null terminated UTF-8 encoded
+ * Unicode string.
+ * @param hValue Value to store in the object
+ *
+ * @return True on success.
+ */
+native bool:json_object_set(Handle:hObj, const String:sKey[], Handle:hValue);
+
+/**
+ * Set the value of sKey to hValue in hObj.
+ * If there already is a value for key, it is replaced by the new value.
+ * This function automatically closes the Handle to the value object.
+ *
+ * @param hObj Handle to JSON object to set a value on
+ * @param sKey Key to store in the object
+ * Must be a valid null terminated UTF-8 encoded
+ * Unicode string.
+ * @param hValue Value to store in the object
+ *
+ * @return True on success.
+ */
+native bool:json_object_set_new(Handle:hObj, const String:sKey[], Handle:hValue);
+
+/**
+ * Delete sKey from hObj if it exists.
+ *
+ * @param hObj Handle to JSON object to delete a key from
+ * @param sKey Key to delete
+ *
+ * @return True on success.
+ */
+native bool:json_object_del(Handle:hObj, const String:sKey[]);
+
+/**
+ * Remove all elements from hObj.
+ *
+ * @param hObj Handle to JSON object to remove all
+ * elements from.
+ *
+ * @return True on success.
+ */
+native bool:json_object_clear(Handle:hObj);
+
+/**
+ * Update hObj with the key-value pairs from hOther, overwriting
+ * existing keys.
+ *
+ * @param hObj Handle to JSON object to update
+ * @param hOther Handle to JSON object to get update
+ * keys/values from.
+ *
+ * @return True on success.
+ */
+native bool:json_object_update(Handle:hObj, Handle:hOther);
+
+/**
+ * Like json_object_update(), but only the values of existing keys
+ * are updated. No new keys are created.
+ *
+ * @param hObj Handle to JSON object to update
+ * @param hOther Handle to JSON object to get update
+ * keys/values from.
+ *
+ * @return True on success.
+ */
+native bool:json_object_update_existing(Handle:hObj, Handle:hOther);
+
+/**
+ * Like json_object_update(), but only new keys are created.
+ * The value of any existing key is not changed.
+ *
+ * @param hObj Handle to JSON object to update
+ * @param hOther Handle to JSON object to get update
+ * keys/values from.
+ *
+ * @return True on success.
+ */
+native bool:json_object_update_missing(Handle:hObj, Handle:hOther);
+
+
+
+
+/**
+ * Object iteration
+ *
+ * Example code:
+ * - We assume hObj is a Handle to a valid JSON object.
+ *
+ *
+ * new Handle:hIterator = json_object_iter(hObj);
+ * while(hIterator != INVALID_HANDLE)
+ * {
+ * new String:sKey[128];
+ * json_object_iter_key(hIterator, sKey, sizeof(sKey));
+ *
+ * new Handle:hValue = json_object_iter_value(hIterator);
+ *
+ * // Do something with sKey and hValue
+ *
+ * CloseHandle(hValue);
+ *
+ * hIterator = json_object_iter_next(hObj, hIterator);
+ * }
+ *
+ */
+
+/**
+ * Returns a handle to an iterator which can be used to iterate over
+ * all key-value pairs in hObj.
+ * If you are not iterating to the end of hObj make sure to close the
+ * handle to the iterator manually.
+ *
+ * @param hObj Handle to JSON object to get an iterator
+ * for.
+ *
+ * @return Handle to JSON object iterator,
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_object_iter(Handle:hObj);
+
+/**
+ * Like json_object_iter(), but returns an iterator to the key-value
+ * pair in object whose key is equal to key.
+ * Iterating forward to the end of object only yields all key-value
+ * pairs of the object if key happens to be the first key in the
+ * underlying hash table.
+ *
+ * @param hObj Handle to JSON object to get an iterator
+ * for.
+ * @param sKey Start key for the iterator
+ *
+ * @return Handle to JSON object iterator,
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_object_iter_at(Handle:hObj, const String:key[]);
+
+/**
+ * Returns an iterator pointing to the next key-value pair in object.
+ * This automatically closes the Handle to the iterator hIter.
+ *
+ * @param hObj Handle to JSON object.
+ * @param hIter Handle to JSON object iterator.
+ *
+ * @return Handle to JSON object iterator,
+ * or INVALID_HANDLE on error, or if the
+ * whole object has been iterated through.
+ */
+native Handle:json_object_iter_next(Handle:hObj, Handle:hIter);
+
+/**
+ * Extracts the associated key of hIter as a null terminated UTF-8
+ * encoded string in the passed buffer.
+ *
+ * @param hIter Handle to the JSON String object
+ * @param sKeyBuffer Buffer to store the value of the String.
+ * @param maxlength Maximum length of string buffer.
+ * @error Invalid JSON Object Iterator.
+ * @return Length of the returned string or -1 on error.
+ */
+native json_object_iter_key(Handle:hIter, String:sKeyBuffer[], maxlength);
+
+/**
+ * Returns a handle to the value hIter is pointing at.
+ *
+ * @param hIter Handle to JSON object iterator.
+ *
+ * @return Handle to value or INVALID_HANDLE on error.
+ */
+native Handle:json_object_iter_value(Handle:hIter);
+
+/**
+ * Set the value of the key-value pair in hObj, that is pointed to
+ * by hIter, to hValue.
+ *
+ * @param hObj Handle to JSON object.
+ * @param hIter Handle to JSON object iterator.
+ * @param hValue Handle to JSON value.
+ *
+ * @return True on success.
+ */
+native bool:json_object_iter_set(Handle:hObj, Handle:hIter, Handle:hValue);
+
+/**
+ * Set the value of the key-value pair in hObj, that is pointed to
+ * by hIter, to hValue.
+ * This function automatically closes the Handle to the value object.
+ *
+ * @param hObj Handle to JSON object.
+ * @param hIter Handle to JSON object iterator.
+ * @param hValue Handle to JSON value.
+ *
+ * @return True on success.
+ */
+native bool:json_object_iter_set_new(Handle:hObj, Handle:hIter, Handle:hValue);
+
+
+
+
+/**
+ * Arrays
+ *
+ * A JSON array is an ordered collection of other JSON values.
+ *
+ */
+
+/**
+ * Returns a handle to a new JSON array, or INVALID_HANDLE on error.
+ *
+ * @return Handle to the new JSON array
+ */
+native Handle:json_array();
+
+/**
+ * Returns the number of elements in hArray
+ *
+ * @param hObj Handle to JSON array
+ *
+ * @return Number of elements in hArray,
+ * or 0 if hObj is not a JSON array.
+ */
+native json_array_size(Handle:hArray);
+
+/**
+ * Returns the element in hArray at position iIndex.
+ *
+ * @param hArray Handle to JSON array to get a value from
+ * @param iIndex Position to retrieve
+ *
+ * @return Handle to a the JSON object or
+ * INVALID_HANDLE on error.
+ */
+native Handle:json_array_get(Handle:hArray, iIndex);
+
+/**
+ * Replaces the element in array at position iIndex with hValue.
+ * The valid range for iIndex is from 0 to the return value of
+ * json_array_size() minus 1.
+ *
+ * @param hArray Handle to JSON array
+ * @param iIndex Position to replace
+ * @param hValue Value to store in the array
+ *
+ * @return True on success.
+ */
+native bool:json_array_set(Handle:hArray, iIndex, Handle:hValue);
+
+/**
+ * Replaces the element in array at position iIndex with hValue.
+ * The valid range for iIndex is from 0 to the return value of
+ * json_array_size() minus 1.
+ * This function automatically closes the Handle to the value object.
+ *
+ * @param hArray Handle to JSON array
+ * @param iIndex Position to replace
+ * @param hValue Value to store in the array
+ *
+ * @return True on success.
+ */
+native bool:json_array_set_new(Handle:hArray, iIndex, Handle:hValue);
+
+/**
+ * Appends value to the end of array, growing the size of array by 1.
+ *
+ * @param hArray Handle to JSON array
+ * @param hValue Value to append to the array
+ *
+ * @return True on success.
+ */
+native bool:json_array_append(Handle:hArray, Handle:hValue);
+
+/**
+ * Appends value to the end of array, growing the size of array by 1.
+ * This function automatically closes the Handle to the value object.
+ *
+ * @param hArray Handle to JSON array
+ * @param hValue Value to append to the array
+ *
+ * @return True on success.
+ */
+native bool:json_array_append_new(Handle:hArray, Handle:hValue);
+
+/**
+ * Inserts value to hArray at position iIndex, shifting the elements at
+ * iIndex and after it one position towards the end of the array.
+ *
+ * @param hArray Handle to JSON array
+ * @param iIndex Position to insert at
+ * @param hValue Value to store in the array
+ *
+ * @return True on success.
+ */
+native bool:json_array_insert(Handle:hArray, iIndex, Handle:hValue);
+
+/**
+ * Inserts value to hArray at position iIndex, shifting the elements at
+ * iIndex and after it one position towards the end of the array.
+ * This function automatically closes the Handle to the value object.
+ *
+ * @param hArray Handle to JSON array
+ * @param iIndex Position to insert at
+ * @param hValue Value to store in the array
+ *
+ * @return True on success.
+ */
+native bool:json_array_insert_new(Handle:hArray, iIndex, Handle:hValue);
+
+/**
+ * Removes the element in hArray at position iIndex, shifting the
+ * elements after iIndex one position towards the start of the array.
+ *
+ * @param hArray Handle to JSON array
+ * @param iIndex Position to insert at
+ *
+ * @return True on success.
+ */
+native bool:json_array_remove(Handle:hArray, iIndex);
+
+/**
+ * Removes all elements from hArray.
+ *
+ * @param hArray Handle to JSON array
+ *
+ * @return True on success.
+ */
+native bool:json_array_clear(Handle:hArray);
+
+/**
+ * Appends all elements in hOther to the end of hArray.
+ *
+ * @param hArray Handle to JSON array to be extended
+ * @param hOther Handle to JSON array, source to copy from
+ *
+ * @return True on success.
+ */
+native bool:json_array_extend(Handle:hArray, Handle:hOther);
+
+
+
+
+/**
+ * Booleans & NULL
+ *
+ */
+
+/**
+ * Returns a handle to a new JSON Boolean with value true,
+ * or INVALID_HANDLE on error.
+ *
+ * @return Handle to the new Boolean object
+ */
+native Handle:json_true();
+
+/**
+ * Returns a handle to a new JSON Boolean with value false,
+ * or INVALID_HANDLE on error.
+ *
+ * @return Handle to the new Boolean object
+ */
+native Handle:json_false();
+
+/**
+ * Returns a handle to a new JSON Boolean with the value passed
+ * in bState or INVALID_HANDLE on error.
+ *
+ * @param bState Value for the new Boolean object
+ * @return Handle to the new Boolean object
+ */
+native Handle:json_boolean(bool:bState);
+
+/**
+ * Returns a handle to a new JSON NULL or INVALID_HANDLE on error.
+ *
+ * @return Handle to the new NULL object
+ */
+native Handle:json_null();
+
+
+
+
+/**
+ * Strings
+ *
+ * Jansson uses UTF-8 as the character encoding. All JSON strings must
+ * be valid UTF-8 (or ASCII, as it�s a subset of UTF-8). Normal null
+ * terminated C strings are used, so JSON strings may not contain
+ * embedded null characters.
+ *
+ */
+
+/**
+ * Returns a handle to a new JSON string, or INVALID_HANDLE on error.
+ *
+ * @param sValue Value for the new String object
+ * Must be a valid UTF-8 encoded Unicode string.
+ * @return Handle to the new String object
+ */
+native Handle:json_string(const String:sValue[]);
+
+/**
+ * Saves the associated value of hString as a null terminated UTF-8
+ * encoded string in the passed buffer.
+ *
+ * @param hString Handle to the JSON String object
+ * @param sValueBuffer Buffer to store the value of the String.
+ * @param maxlength Maximum length of string buffer.
+ * @error Invalid JSON String Object.
+ * @return Length of the returned string or -1 on error.
+ */
+native json_string_value(Handle:hString, String:sValueBuffer[], maxlength);
+
+/**
+ * Sets the associated value of JSON String object to value.
+ *
+ * @param hString Handle to the JSON String object
+ * @param sValue Value to set the object to.
+ * Must be a valid UTF-8 encoded Unicode string.
+ * @error Invalid JSON String Object.
+ * @return True on success.
+ */
+native bool:json_string_set(Handle:hString, String:sValue[]);
+
+
+
+
+/**
+ * Numbers
+ *
+ * The JSON specification only contains one numeric type, 'number'.
+ * The C (and Pawn) programming language has distinct types for integer
+ * and floating-point numbers, so for practical reasons Jansson also has
+ * distinct types for the two. They are called 'integer' and 'real',
+ * respectively. (Whereas 'real' is a 'Float' for Pawn).
+ * Therefore a number is represented by either a value of the type
+ * JSON_INTEGER or of the type JSON_REAL.
+ *
+ */
+
+/**
+ * Returns a handle to a new JSON integer, or INVALID_HANDLE on error.
+ *
+ * @param iValue Value for the new Integer object
+ * @return Handle to the new Integer object
+ */
+native Handle:json_integer(iValue);
+
+/**
+ * Returns the associated value of a JSON Integer Object.
+ *
+ * @param hInteger Handle to the JSON Integer object
+ * @error Invalid JSON Integer Object.
+ * @return Value of the hInteger,
+ * or 0 if hInteger is not a JSON integer.
+ */
+native json_integer_value(Handle:hInteger);
+
+/**
+ * Sets the associated value of JSON Integer to value.
+ *
+ * @param hInteger Handle to the JSON Integer object
+ * @param iValue Value to set the object to.
+ * @error Invalid JSON Integer Object.
+ * @return True on success.
+ */
+native bool:json_integer_set(Handle:hInteger, iValue);
+
+/**
+ * Returns a handle to a new JSON real, or INVALID_HANDLE on error.
+ *
+ * @param fValue Value for the new Real object
+ * @return Handle to the new String object
+ */
+native Handle:json_real(Float:fValue);
+
+/**
+ * Returns the associated value of a JSON Real.
+ *
+ * @param hReal Handle to the JSON Real object
+ * @error Invalid JSON Real Object.
+ * @return Float value of hReal,
+ * or 0.0 if hReal is not a JSON Real.
+ */
+native Float:json_real_value(Handle:hReal);
+
+/**
+ * Sets the associated value of JSON Real to fValue.
+ *
+ * @param hReal Handle to the JSON Integer object
+ * @param fValue Value to set the object to.
+ * @error Invalid JSON Real handle.
+ * @return True on success.
+ */
+native bool:json_real_set(Handle:hReal, Float:value);
+
+/**
+ * Returns the associated value of a JSON integer or a
+ * JSON Real, cast to Float regardless of the actual type.
+ *
+ * @param hNumber Handle to the JSON Number
+ * @error Not a JSON Real or JSON Integer
+ * @return Float value of hNumber,
+ * or 0.0 on error.
+ */
+native Float:json_number_value(Handle:hNumber);
+
+
+
+
+/**
+ * Decoding
+ *
+ * This sections describes the functions that can be used to decode JSON text
+ * to the Jansson representation of JSON data. The JSON specification requires
+ * that a JSON text is either a serialized array or object, and this
+ * requirement is also enforced with the following functions. In other words,
+ * the top level value in the JSON text being decoded must be either array or
+ * object.
+ *
+ */
+
+/**
+ * Decodes the JSON string sJSON and returns the array or object it contains.
+ * Errors while decoding can be found in the sourcemod error log.
+ *
+ * @param sJSON String containing valid JSON
+
+ * @return Handle to JSON object or array.
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_load(const String:sJSON[]);
+
+/**
+ * Decodes the JSON string sJSON and returns the array or object it contains.
+ * This function provides additional error feedback and does not log errors
+ * to the sourcemod error log.
+ *
+ * @param sJSON String containing valid JSON
+ * @param sErrorText This buffer will be filled with the error
+ * message.
+ * @param maxlen Size of the buffer
+ * @param iLine This int will contain the line of the error
+ * @param iColumn This int will contain the column of the error
+ *
+ * @return Handle to JSON object or array.
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_load_ex(const String:sJSON[], String:sErrorText[], maxlen, &iLine, &iColumn);
+
+/**
+ * Decodes the JSON text in file sFilePath and returns the array or object
+ * it contains.
+ * Errors while decoding can be found in the sourcemod error log.
+ *
+ * @param sFilePath Path to a file containing pure JSON
+ *
+ * @return Handle to JSON object or array.
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_load_file(const String:sFilePath[PLATFORM_MAX_PATH]);
+
+/**
+ * Decodes the JSON text in file sFilePath and returns the array or object
+ * it contains.
+ * This function provides additional error feedback and does not log errors
+ * to the sourcemod error log.
+ *
+ * @param sFilePath Path to a file containing pure JSON
+ * @param sErrorText This buffer will be filled with the error
+ * message.
+ * @param maxlen Size of the buffer
+ * @param iLine This int will contain the line of the error
+ * @param iColumn This int will contain the column of the error
+ *
+ * @return Handle to JSON object or array.
+ * or INVALID_HANDLE on error.
+ */
+native Handle:json_load_file_ex(const String:sFilePath[PLATFORM_MAX_PATH], String:sErrorText[], maxlen, &iLine, &iColumn);
+
+
+
+/**
+ * Encoding
+ *
+ * This sections describes the functions that can be used to encode values
+ * to JSON. By default, only objects and arrays can be encoded directly,
+ * since they are the only valid root values of a JSON text.
+ *
+ */
+
+/**
+ * Saves the JSON representation of hObject in sJSON.
+ *
+ * @param hObject String containing valid JSON
+ * @param sJSON Buffer to store the created JSON string.
+ * @param maxlength Maximum length of string buffer.
+ * @param iIndentWidth Indenting with iIndentWidth spaces.
+ * The valid range for this is between 0 and 31 (inclusive),
+ * other values result in an undefined output. If this is set
+ * to 0, no newlines are inserted between array and object items.
+ * @param bEnsureAscii If this is set, the output is guaranteed
+ * to consist only of ASCII characters. This is achieved
+ * by escaping all Unicode characters outside the ASCII range.
+ * @param bSortKeys If this flag is used, all the objects in output are sorted
+ * by key. This is useful e.g. if two JSON texts are diffed
+ * or visually compared.
+ * @param bPreserveOrder If this flag is used, object keys in the output are sorted
+ * into the same order in which they were first inserted to
+ * the object. For example, decoding a JSON text and then
+ * encoding with this flag preserves the order of object keys.
+ * @return Length of the returned string or -1 on error.
+ */
+native json_dump(Handle:hObject, String:sJSON[], maxlength, iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false);
+
+/**
+ * Write the JSON representation of hObject to the file sFilePath.
+ * If sFilePath already exists, it is overwritten.
+ *
+ * @param hObject String containing valid JSON
+ * @param sFilePath Buffer to store the created JSON string.
+ * @param iIndentWidth Indenting with iIndentWidth spaces.
+ * The valid range for this is between 0 and 31 (inclusive),
+ * other values result in an undefined output. If this is set
+ * to 0, no newlines are inserted between array and object items.
+ * @param bEnsureAscii If this is set, the output is guaranteed
+ * to consist only of ASCII characters. This is achieved
+ * by escaping all Unicode characters outside the ASCII range.
+ * @param bSortKeys If this flag is used, all the objects in output are sorted
+ * by key. This is useful e.g. if two JSON texts are diffed
+ * or visually compared.
+ * @param bPreserveOrder If this flag is used, object keys in the output are sorted
+ * into the same order in which they were first inserted to
+ * the object. For example, decoding a JSON text and then
+ * encoding with this flag preserves the order of object keys.
+ * @return Length of the returned string or -1 on error.
+ */
+native bool:json_dump_file(Handle:hObject, const String:sFilePath[], iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false);
+
+
+
+/**
+ * Convenience stocks
+ *
+ * These are some custom functions to ease the development using this
+ * extension.
+ *
+ */
+
+/**
+ * Returns a handle to a new JSON string, or INVALID_HANDLE on error.
+ * Formats the string according to the SourceMod format rules.
+ * The result must be a valid UTF-8 encoded Unicode string.
+ *
+ * @param sFormat Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @return Handle to the new String object
+ */
+stock Handle:json_string_format(const String:sFormat[], any:...) {
+ new String:sTmp[4096];
+ VFormat(sTmp, sizeof(sTmp), sFormat, 2);
+
+ return json_string(sTmp);
+}
+
+/**
+ * Returns a handle to a new JSON string, or INVALID_HANDLE on error.
+ * This stock allows to specify the size of the temporary buffer used
+ * to create the string. Use this if the default of 4096 is not enough
+ * for your string.
+ * Formats the string according to the SourceMod format rules.
+ * The result must be a valid UTF-8 encoded Unicode string.
+ *
+ * @param tmpBufferLength Size of the temporary buffer
+ * @param sFormat Formatting rules.
+ * @param ... Variable number of format parameters.
+ * @return Handle to the new String object
+ */
+stock Handle:json_string_format_ex(tmpBufferLength, const String:sFormat[], any:...) {
+ new String:sTmp[tmpBufferLength];
+ VFormat(sTmp, sizeof(sTmp), sFormat, 3);
+
+ return json_string(sTmp);
+}
+
+
+/**
+ * Returns the boolean value of the element in hArray at position iIndex.
+ *
+ * @param hArray Handle to JSON array to get a value from
+ * @param iIndex Position to retrieve
+ *
+ * @return True if it's a boolean and TRUE,
+ * false otherwise.
+ */
+stock bool:json_array_get_bool(Handle:hArray, iIndex) {
+ new Handle:hElement = json_array_get(hArray, iIndex);
+
+ new bool:bResult = (json_is_true(hElement) ? true : false);
+
+ CloseHandle(hElement);
+ return bResult;
+}
+
+/**
+ * Returns the float value of the element in hArray at position iIndex.
+ *
+ * @param hArray Handle to JSON array to get a value from
+ * @param iIndex Position to retrieve
+ *
+ * @return Float value,
+ * or 0.0 if element is not a JSON Real.
+ */
+stock Float:json_array_get_float(Handle:hArray, iIndex) {
+ new Handle:hElement = json_array_get(hArray, iIndex);
+
+ new Float:fResult = (json_is_number(hElement) ? json_number_value(hElement) : 0.0);
+
+ CloseHandle(hElement);
+ return fResult;
+}
+
+/**
+ * Returns the integer value of the element in hArray at position iIndex.
+ *
+ * @param hArray Handle to JSON array to get a value from
+ * @param iIndex Position to retrieve
+ *
+ * @return Integer value,
+ * or 0 if element is not a JSON Integer.
+ */
+stock json_array_get_int(Handle:hArray, iIndex) {
+ new Handle:hElement = json_array_get(hArray, iIndex);
+
+ new iResult = (json_is_integer(hElement) ? json_integer_value(hElement) : 0);
+
+ CloseHandle(hElement);
+ return iResult;
+}
+
+/**
+ * Saves the associated value of the element in hArray at position iIndex
+ * as a null terminated UTF-8 encoded string in the passed buffer.
+ *
+ * @param hArray Handle to JSON array to get a value from
+ * @param iIndex Position to retrieve
+ * @param sBuffer Buffer to store the value of the String.
+ * @param maxlength Maximum length of string buffer.
+ *
+ * @error Element is not a JSON String.
+ * @return Length of the returned string or -1 on error.
+ */
+stock json_array_get_string(Handle:hArray, iIndex, String:sBuffer[], maxlength) {
+ new Handle:hElement = json_array_get(hArray, iIndex);
+
+ new iResult = -1;
+ if(json_is_string(hElement)) {
+ iResult = json_string_value(hElement, sBuffer, maxlength);
+ }
+ CloseHandle(hElement);
+
+ return iResult;
+}
+
+/**
+ * Returns the boolean value of the element in hObj at entry sKey.
+ *
+ * @param hObj Handle to JSON object to get a value from
+ * @param sKey Entry to retrieve
+ *
+ * @return True if it's a boolean and TRUE,
+ * false otherwise.
+ */
+stock bool:json_object_get_bool(Handle:hObj, const String:sKey[]) {
+ new Handle:hElement = json_object_get(hObj, sKey);
+
+ new bool:bResult = (json_is_true(hElement) ? true : false);
+
+ CloseHandle(hElement);
+ return bResult;
+}
+
+/**
+ * Returns the float value of the element in hObj at entry sKey.
+ *
+ * @param hObj Handle to JSON object to get a value from
+ * @param sKey Position to retrieve
+ *
+ * @return Float value,
+ * or 0.0 if element is not a JSON Real.
+ */
+stock Float:json_object_get_float(Handle:hObj, const String:sKey[]) {
+ new Handle:hElement = json_object_get(hObj, sKey);
+
+ new Float:fResult = (json_is_number(hElement) ? json_number_value(hElement) : 0.0);
+
+ CloseHandle(hElement);
+ return fResult;
+}
+
+/**
+ * Returns the integer value of the element in hObj at entry sKey.
+ *
+ * @param hObj Handle to JSON object to get a value from
+ * @param sKey Position to retrieve
+ *
+ * @return Integer value,
+ * or 0 if element is not a JSON Integer.
+ */
+stock json_object_get_int(Handle:hObj, const String:sKey[]) {
+ new Handle:hElement = json_object_get(hObj, sKey);
+
+ new iResult = (json_is_integer(hElement) ? json_integer_value(hElement) : 0);
+
+ CloseHandle(hElement);
+ return iResult;
+}
+
+/**
+ * Saves the associated value of the element in hObj at entry sKey
+ * as a null terminated UTF-8 encoded string in the passed buffer.
+ *
+ * @param hObj Handle to JSON object to get a value from
+ * @param sKey Entry to retrieve
+ * @param sBuffer Buffer to store the value of the String.
+ * @param maxlength Maximum length of string buffer.
+ *
+ * @error Element is not a JSON String.
+ * @return Length of the returned string or -1 on error.
+ */
+stock json_object_get_string(Handle:hObj, const String:sKey[], String:sBuffer[], maxlength) {
+ new Handle:hElement = json_object_get(hObj, sKey);
+
+ new iResult = -1;
+ if(json_is_string(hElement)) {
+ iResult = json_string_value(hElement, sBuffer, maxlength);
+ }
+ CloseHandle(hElement);
+
+ return iResult;
+}
+
+
+
+/**
+ * Pack String Rules
+ *
+ * Here�s the full list of format characters:
+ * n Output a JSON null value. No argument is consumed.
+ * s Output a JSON string, consuming one argument.
+ * b Output a JSON bool value, consuming one argument.
+ * i Output a JSON integer value, consuming one argument.
+ * f Output a JSON real value, consuming one argument.
+ * r Output a JSON real value, consuming one argument.
+ * [] Build an array with contents from the inner format string,
+ * recursive value building is supported.
+ * No argument is consumed.
+ * {} Build an array with contents from the inner format string.
+ * The first, third, etc. format character represent a key,
+ * and must be s (as object keys are always strings). The
+ * second, fourth, etc. format character represent a value.
+ * Recursive value building is supported.
+ * No argument is consumed.
+ *
+ */
+
+/**
+ * This method can be used to create json objects/arrays directly
+ * without having to create the structure.
+ * See 'Pack String Rules' for more details.
+ *
+ * @param sPackString Pack string similiar to Format()s fmt.
+ * See 'Pack String Rules'.
+ * @param hParams ADT Array containing all keys and values
+ * in the order they appear in the pack string.
+ *
+ * @error Invalid pack string or pack string and
+ * ADT Array don't match up regarding type
+ * or size.
+ * @return Handle to JSON element.
+ */
+stock Handle:json_pack(const String:sPackString[], Handle:hParams) {
+ new iPos = 0;
+ return json_pack_element_(sPackString, iPos, hParams);
+}
+
+
+
+
+
+/**
+* Internal stocks used by json_pack(). Don't use these directly!
+*
+*/
+stock Handle:json_pack_array_(const String:sFormat[], &iPos, Handle:hParams) {
+ new Handle:hObj = json_array();
+ new iStrLen = strlen(sFormat);
+ for(; iPos < iStrLen;) {
+ new this_char = sFormat[iPos];
+
+ if(this_char == 32 || this_char == 58 || this_char == 44) {
+ // Skip whitespace, ',' and ':'
+ iPos++;
+ continue;
+ }
+
+ if(this_char == 93) {
+ // array end
+ iPos++;
+ break;
+ }
+
+ // Get the next entry as value
+ // This automatically increments the position!
+ new Handle:hValue = json_pack_element_(sFormat, iPos, hParams);
+
+ // Append the value to the array.
+ json_array_append_new(hObj, hValue);
+ }
+
+ return hObj;
+}
+
+stock Handle:json_pack_object_(const String:sFormat[], &iPos, Handle:hParams) {
+ new Handle:hObj = json_object();
+ new iStrLen = strlen(sFormat);
+ for(; iPos < iStrLen;) {
+ new this_char = sFormat[iPos];
+
+ if(this_char == 32 || this_char == 58 || this_char == 44) {
+ // Skip whitespace, ',' and ':'
+ iPos++;
+ continue;
+ }
+
+ if(this_char == 125) {
+ // } --> object end
+ iPos++;
+ break;
+ }
+
+ if(this_char != 115) {
+ LogError("Object keys must be strings at %d.", iPos);
+ return INVALID_HANDLE;
+ }
+
+ // Get the key string for this object from
+ // the hParams array.
+ decl String:sKey[255];
+ GetArrayString(hParams, 0, sKey, sizeof(sKey));
+ RemoveFromArray(hParams, 0);
+
+ // Advance one character in the pack string,
+ // because we've just read the Key string for
+ // this object.
+ iPos++;
+
+ // Get the next entry as value
+ // This automatically increments the position!
+ new Handle:hValue = json_pack_element_(sFormat, iPos, hParams);
+
+ // Insert into object
+ json_object_set_new(hObj, sKey, hValue);
+ }
+
+ return hObj;
+}
+
+stock Handle:json_pack_element_(const String:sFormat[], &iPos, Handle:hParams) {
+ new this_char = sFormat[iPos];
+ while(this_char == 32 || this_char == 58 || this_char == 44) {
+ iPos++;
+ this_char = sFormat[iPos];
+ }
+
+ // Advance one character in the pack string
+ iPos++;
+
+ switch(this_char) {
+ case 91: {
+ // { --> Array
+ return json_pack_array_(sFormat, iPos, hParams);
+ }
+
+ case 123: {
+ // { --> Object
+ return json_pack_object_(sFormat, iPos, hParams);
+
+ }
+
+ case 98: {
+ // b --> Boolean
+ new iValue = GetArrayCell(hParams, 0);
+ RemoveFromArray(hParams, 0);
+
+ return json_boolean(bool:iValue);
+ }
+
+ case 102, 114: {
+ // r,f --> Real (Float)
+ new Float:iValue = GetArrayCell(hParams, 0);
+ RemoveFromArray(hParams, 0);
+
+ return json_real(iValue);
+ }
+
+ case 110: {
+ // n --> NULL
+ return json_null();
+ }
+
+ case 115: {
+ // s --> String
+ decl String:sKey[255];
+ GetArrayString(hParams, 0, sKey, sizeof(sKey));
+ RemoveFromArray(hParams, 0);
+
+ return json_string(sKey);
+ }
+
+ case 105: {
+ // i --> Integer
+ new iValue = GetArrayCell(hParams, 0);
+ RemoveFromArray(hParams, 0);
+
+ return json_integer(iValue);
+ }
+ }
+
+ SetFailState("Invalid pack String '%s'. Type '%s' not supported at %i", sFormat, this_char, iPos);
+ return json_null();
+}
+
+
+
+
+
+/**
+ * Not yet implemented
+ *
+ * native json_object_foreach(Handle:hObj, ForEachCallback:cb);
+ * native Handle:json_unpack(const String:sFormat[], ...);
+ *
+ */
+
+
+
+
+
+
+/**
+ * Do not edit below this line!
+ */
+public Extension:__ext_smjansson =
+{
+ name = "SMJansson",
+ file = "smjansson.ext",
+#if defined AUTOLOAD_EXTENSIONS
+ autoload = 1,
+#else
+ autoload = 0,
+#endif
+#if defined REQUIRE_EXTENSIONS
+ required = 1,
+#else
+ required = 0,
+#endif
+};
+
+#if !defined REQUIRE_EXTENSIONS
+public __ext_smjansson_SetNTVOptional()
+{
+ MarkNativeAsOptional("json_typeof");
+ MarkNativeAsOptional("json_equal");
+
+ MarkNativeAsOptional("json_copy");
+ MarkNativeAsOptional("json_deep_copy");
+
+ MarkNativeAsOptional("json_object");
+ MarkNativeAsOptional("json_object_size");
+ MarkNativeAsOptional("json_object_get");
+ MarkNativeAsOptional("json_object_set");
+ MarkNativeAsOptional("json_object_set_new");
+ MarkNativeAsOptional("json_object_del");
+ MarkNativeAsOptional("json_object_clear");
+ MarkNativeAsOptional("json_object_update");
+ MarkNativeAsOptional("json_object_update_existing");
+ MarkNativeAsOptional("json_object_update_missing");
+
+ MarkNativeAsOptional("json_object_iter");
+ MarkNativeAsOptional("json_object_iter_at");
+ MarkNativeAsOptional("json_object_iter_next");
+ MarkNativeAsOptional("json_object_iter_key");
+ MarkNativeAsOptional("json_object_iter_value");
+ MarkNativeAsOptional("json_object_iter_set");
+ MarkNativeAsOptional("json_object_iter_set_new");
+
+ MarkNativeAsOptional("json_array");
+ MarkNativeAsOptional("json_array_size");
+ MarkNativeAsOptional("json_array_get");
+ MarkNativeAsOptional("json_array_set");
+ MarkNativeAsOptional("json_array_set_new");
+ MarkNativeAsOptional("json_array_append");
+ MarkNativeAsOptional("json_array_append_new");
+ MarkNativeAsOptional("json_array_insert");
+ MarkNativeAsOptional("json_array_insert_new");
+ MarkNativeAsOptional("json_array_remove");
+ MarkNativeAsOptional("json_array_clear");
+ MarkNativeAsOptional("json_array_extend");
+
+ MarkNativeAsOptional("json_string");
+ MarkNativeAsOptional("json_string_value");
+ MarkNativeAsOptional("json_string_set");
+
+ MarkNativeAsOptional("json_integer");
+ MarkNativeAsOptional("json_integer_value");
+ MarkNativeAsOptional("json_integer_set");
+
+ MarkNativeAsOptional("json_real");
+ MarkNativeAsOptional("json_real_value");
+ MarkNativeAsOptional("json_real_set");
+ MarkNativeAsOptional("json_number_value");
+
+ MarkNativeAsOptional("json_boolean");
+ MarkNativeAsOptional("json_true");
+ MarkNativeAsOptional("json_false");
+ MarkNativeAsOptional("json_null");
+
+ MarkNativeAsOptional("json_load");
+ MarkNativeAsOptional("json_load_file");
+
+ MarkNativeAsOptional("json_dump");
+ MarkNativeAsOptional("json_dump_file");
+}
+#endif
diff --git a/sourcemod/scripting/include/sourcebanspp.inc b/sourcemod/scripting/include/sourcebanspp.inc
new file mode 100644
index 0000000..c984bff
--- /dev/null
+++ b/sourcemod/scripting/include/sourcebanspp.inc
@@ -0,0 +1,106 @@
+// *************************************************************************
+// This file is part of SourceBans++.
+//
+// Copyright (C) 2014-2019 SourceBans++ Dev Team <https://github.com/sbpp>
+//
+// SourceBans++ 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, per version 3 of the License.
+//
+// SourceBans++ 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 SourceBans++. If not, see <http://www.gnu.org/licenses/>.
+//
+// This file based off work(s) covered by the following copyright(s):
+//
+// SourceBans 1.4.11
+// Copyright (C) 2007-2015 SourceBans Team - Part of GameConnect
+// Licensed under GNU GPL version 3, or later.
+// Page: <http://www.sourcebans.net/> - <https://github.com/GameConnect/sourcebansv1>
+//
+// *************************************************************************
+
+#if defined _sourcebanspp_included
+#endinput
+#endif
+#define _sourcebanspp_included
+
+public SharedPlugin __pl_sourcebanspp =
+{
+ name = "sourcebans++",
+ file = "sbpp_main.smx",
+ #if defined REQUIRE_PLUGIN
+ required = 1
+ #else
+ required = 0
+ #endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public void __pl_sourcebanspp_SetNTVOptional()
+{
+ MarkNativeAsOptional("SBBanPlayer");
+ MarkNativeAsOptional("SBPP_BanPlayer");
+ MarkNativeAsOptional("SBPP_ReportPlayer");
+}
+#endif
+
+
+/*********************************************************
+ * Ban Player from server
+ *
+ * @param iAdmin The client index of the admin who is banning the client
+ * @param iTarget The client index of the player to ban
+ * @param iTime The time to ban the player for (in minutes, 0 = permanent)
+ * @param sReason The reason to ban the player from the server
+ * @noreturn
+ *********************************************************/
+#pragma deprecated Use SBPP_BanPlayer() instead.
+native void SBBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason);
+
+/*********************************************************
+ * Ban Player from server
+ *
+ * @param iAdmin The client index of the admin who is banning the client
+ * @param iTarget The client index of the player to ban
+ * @param iTime The time to ban the player for (in minutes, 0 = permanent)
+ * @param sReason The reason to ban the player from the server
+ * @noreturn
+ *********************************************************/
+native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason);
+
+/*********************************************************
+ * Reports a player
+ *
+ * @param iReporter The client index of the reporter
+ * @param iTarget The client index of the player to report
+ * @param sReason The reason to report the player
+ * @noreturn
+ *********************************************************/
+native void SBPP_ReportPlayer(int iReporter, int iTarget, const char[] sReason);
+
+/*********************************************************
+ * Called when the admin banning the player.
+ *
+ * @param iAdmin The client index of the admin who is banning the client
+ * @param iTarget The client index of the player to ban
+ * @param iTime The time to ban the player for (in minutes, 0 = permanent)
+ * @param sReason The reason to ban the player from the server
+ *********************************************************/
+forward void SBPP_OnBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason);
+
+/*********************************************************
+ * Called when a new report is inserted
+ *
+ * @param iReporter The client index of the reporter
+ * @param iTarget The client index of the player to report
+ * @param sReason The reason to report the player
+ * @noreturn
+ *********************************************************/
+forward void SBPP_OnReportPlayer(int iReporter, int iTarget, const char[] sReason);
+
+//Yarr!
diff --git a/sourcemod/scripting/include/sourcemod-colors.inc b/sourcemod/scripting/include/sourcemod-colors.inc
new file mode 100644
index 0000000..66bc97b
--- /dev/null
+++ b/sourcemod/scripting/include/sourcemod-colors.inc
@@ -0,0 +1,921 @@
+#if defined _sourcemod_colors_included
+ #endinput
+#endif
+#define _sourcemod_colors_included "1.0"
+
+/*
+* _____ _ _____ _
+* / ____| | | / ____| | |
+* | (___ ___ _ _ _ __ ___ ___ _ __ ___ ___ __| | | | ___ | | ___ _ __ ___
+* \___ \ / _ \| | | | '__/ __/ _ \ '_ ` _ \ / _ \ / _` | | | / _ \| |/ _ \| '__/ __|
+* ____) | (_) | |_| | | | (_| __/ | | | | | (_) | (_| | | |___| (_) | | (_) | | \__ \
+* |_____/ \___/ \__,_|_| \___\___|_| |_| |_|\___/ \__,_| \_____\___/|_|\___/|_| |___/
+*
+*
+* - Author: Keith Warren (Drixevel)
+* - Original By: Raska aka KissLick (ColorVariables)
+*
+* This is meant to be a drop-in replacement for every Source Engine game to add colors to chat and more cheat features.
+*/
+
+// ----------------------------------------------------------------------------------------
+#define MAX_BUFFER_SIZE 1024
+
+static bool g_bInit;
+static StringMap g_hColors;
+static char g_sChatPrefix[64];
+
+static bool g_bIgnorePrefix;
+static int g_iAuthor;
+static bool g_bSkipPlayers[MAXPLAYERS + 1];
+// ----------------------------------------------------------------------------------------
+
+/*
+* Sets the prefix for all chat prints to use.
+*
+* prefix - String to use.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CSetPrefix(const char[] prefix, any ...)
+{
+ VFormat(g_sChatPrefix, sizeof(g_sChatPrefix), prefix, 2);
+}
+
+/*
+* Setup the next print to skip using the prefix.
+*
+*
+* Return - N/A
+*/
+stock void CSkipNextPrefix()
+{
+ g_bIgnorePrefix = true;
+}
+
+/*
+* Sets the author for the next print. (Mostly applies colors)
+*
+* client - Author index.
+*
+* Return - N/A
+*/
+stock void CSetNextAuthor(int client)
+{
+ if (client < 1 || client > MaxClients || !IsClientInGame(client))
+ ThrowError("Invalid client index %i", client);
+
+ g_iAuthor = client;
+}
+
+/*
+* Setup the next chat print to not be sent to this client.
+*
+* client - Client index.
+*
+* Return - N/A
+*/
+stock void CSkipNextClient(int client)
+{
+ if (client < 1 || client > MaxClients)
+ ThrowError("Invalid client index %i", client);
+
+ g_bSkipPlayers[client] = true;
+}
+
+/*
+* Sends a chat print to the client.
+*
+* client - Client index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChat(int client, const char[] message, any ...)
+{
+ if ((client < 1 || client > MaxClients || !IsClientInGame(client) || IsFakeClient(client)) && !IsClientSourceTV(client))
+ return;
+
+ SetGlobalTransTarget(client);
+
+ char buffer[MAX_BUFFER_SIZE];
+ VFormat(buffer, sizeof(buffer), message, 3);
+
+ AddPrefixAndDefaultColor(buffer, sizeof(buffer));
+ g_bIgnorePrefix = false;
+
+ CProcessVariables(buffer, sizeof(buffer));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+
+ SendPlayerMessage(client, buffer, g_iAuthor);
+ g_iAuthor = 0;
+}
+
+/*
+* Sends a chat print to the client with a specified author.
+*
+* client - Client index.
+* author - Author index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatEx(int client, int author, const char[] message, any ...)
+{
+ CSetNextAuthor(author);
+ char buffer[MAX_BUFFER_SIZE];
+ VFormat(buffer, sizeof(buffer), message, 4);
+ CPrintToChat(client, buffer);
+}
+
+/*
+* Sends a chat print to all clients.
+*
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatAll(const char[] message, any ...)
+{
+ char buffer[MAX_BUFFER_SIZE];
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (!IsClientInGame(client) || g_bSkipPlayers[client])
+ {
+ g_bSkipPlayers[client] = false;
+ continue;
+ }
+
+ SetGlobalTransTarget(client);
+
+ VFormat(buffer, sizeof(buffer), message, 2);
+
+ AddPrefixAndDefaultColor(buffer, sizeof(buffer));
+ g_bIgnorePrefix = false;
+
+ CProcessVariables(buffer, sizeof(buffer));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+
+ SendPlayerMessage(client, buffer, g_iAuthor);
+ }
+
+ g_iAuthor = 0;
+}
+
+/*
+* Sends a chat print to all clients with a specified author.
+*
+* author - Author index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatAllEx(int author, const char[] message, any ...)
+{
+ CSetNextAuthor(author);
+ char buffer[MAX_BUFFER_SIZE];
+ VFormat(buffer, sizeof(buffer), message, 3);
+ CPrintToChatAll(buffer);
+}
+
+/*
+* Sends a chat print to a specified team.
+*
+* team - Team index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatTeam(int team, const char[] message, any ...)
+{
+ char buffer[MAX_BUFFER_SIZE];
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (!IsClientInGame(client) || GetClientTeam(client) != team || g_bSkipPlayers[client])
+ {
+ g_bSkipPlayers[client] = false;
+ continue;
+ }
+
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), message, 3);
+
+ AddPrefixAndDefaultColor(buffer, sizeof(buffer));
+ g_bIgnorePrefix = false;
+
+ CProcessVariables(buffer, sizeof(buffer));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+
+ SendPlayerMessage(client, buffer, g_iAuthor);
+ }
+
+ g_iAuthor = 0;
+}
+
+/*
+* Sends a chat print to a specified team with a specified author.
+*
+* team - Team index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatTeamEx(int team, int author, const char[] message, any ...)
+{
+ CSetNextAuthor(author);
+ char buffer[MAX_BUFFER_SIZE];
+ VFormat(buffer, sizeof(buffer), message, 4);
+ CPrintToChatTeam(team, buffer);
+}
+
+/*
+* Sends a chat print to available admins.
+* Example for bitflags: (ADMFLAG_RESERVATION | ADMFLAG_GENERIC)
+*
+* bitflags - Bit Flags.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatAdmins(int bitflags, const char[] message, any ...)
+{
+ char buffer[MAX_BUFFER_SIZE];
+ AdminId iAdminID;
+
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (!IsClientInGame(client) || g_bSkipPlayers[client])
+ {
+ g_bSkipPlayers[client] = false;
+ continue;
+ }
+
+ iAdminID = GetUserAdmin(client);
+
+ if (iAdminID == INVALID_ADMIN_ID || !(GetAdminFlags(iAdminID, Access_Effective) & bitflags))
+ continue;
+
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), message, 3);
+
+ AddPrefixAndDefaultColor(buffer, sizeof(buffer));
+ g_bIgnorePrefix = false;
+
+ CProcessVariables(buffer, sizeof(buffer));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+
+ SendPlayerMessage(client, buffer, g_iAuthor);
+ }
+
+ g_iAuthor = 0;
+}
+
+/*
+* Sends a chat print to available admins with a specified author.
+* Example for bitflags: (ADMFLAG_RESERVATION | ADMFLAG_GENERIC)
+*
+* bitflags - Bit Flags.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CPrintToChatAdminsEx(int bitflags, int author, const char[] message, any ...)
+{
+ CSetNextAuthor(author);
+ char buffer[MAX_BUFFER_SIZE];
+ VFormat(buffer, sizeof(buffer), message, 4);
+ CPrintToChatTeam(bitflags, buffer);
+}
+
+/*
+* Sends a reply message to the client. (This is useful because it works for console as well)
+*
+* client - Client index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CReplyToCommand(int client, const char[] message, any ...)
+{
+ if (client < 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ if (client != 0 && !IsClientInGame(client))
+ ThrowError("Client %d is not in game", client);
+
+ char buffer[MAX_BUFFER_SIZE];
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), message, 3);
+
+ AddPrefixAndDefaultColor(buffer, sizeof(buffer), "engine 1");
+ g_bIgnorePrefix = false;
+
+ if (GetCmdReplySource() == SM_REPLY_TO_CONSOLE)
+ {
+ CRemoveColors(buffer, sizeof(buffer));
+ PrintToConsole(client, "%s", buffer);
+ }
+ else
+ CPrintToChat(client, "%s", buffer);
+}
+
+/*
+* Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar.
+* This version does not display a message to the originating client if used from chat triggers or menus.
+* If manual replies are used for these cases, then this function will suffice.
+* Otherwise, ShowActivity2() is slightly more useful.
+*
+* client - Client index.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CShowActivity(int client, const char[] message, any ...)
+{
+ if (client < 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ if (client != 0 && !IsClientInGame(client))
+ ThrowError("Client %d is not in game", client);
+
+ char buffer[MAX_BUFFER_SIZE];
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), message, 3);
+ Format(buffer, sizeof(buffer), "{engine 1}%s", buffer);
+ CProcessVariables(buffer, sizeof(buffer));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+
+ ShowActivity(client, "%s", buffer);
+}
+
+/*
+* Displays usage of an admin command to users depending on the setting of the sm_show_activity cvar.
+* All users receive a message in their chat text, except for the originating client, who receives the message based on the current ReplySource.
+*
+* client - Client index.
+* tag - Tag to show.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CShowActivityEx(int client, const char[] tag, const char[] message, any ...)
+{
+ if (client < 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ if (client != 0 && !IsClientInGame(client))
+ ThrowError("Client %d is not in game", client);
+
+ char buffer[MAX_BUFFER_SIZE]; char sBufferTag[MAX_BUFFER_SIZE];
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), message, 4);
+ Format(buffer, sizeof(buffer), "{engine 1}%s", buffer);
+ CProcessVariables(buffer, sizeof(buffer));
+ Format(sBufferTag, sizeof(sBufferTag), "{prefix}%s", tag);
+ CProcessVariables(sBufferTag, sizeof(sBufferTag));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+ CAddWhiteSpace(sBufferTag, sizeof(sBufferTag));
+
+ ShowActivityEx(client, sBufferTag, " %s", buffer);
+}
+
+/*
+* Same as ShowActivity(), except the tag parameter is used instead of "[SM] " (note that you must supply any spacing).
+*
+* client - Client index.
+* tag - Tag to show.
+* message - Message string.
+* any - Extra Parameters
+*
+* Return - N/A
+*/
+stock void CShowActivity2(int client, const char[] tag, const char[] message, any ...)
+{
+ if (client < 0 || client > MaxClients)
+ ThrowError("Invalid client index %d", client);
+
+ if (client != 0 && !IsClientInGame(client))
+ ThrowError("Client %d is not in game", client);
+
+ char buffer[MAX_BUFFER_SIZE]; char sBufferTag[MAX_BUFFER_SIZE];
+ SetGlobalTransTarget(client);
+ VFormat(buffer, sizeof(buffer), message, 4);
+ Format(buffer, sizeof(buffer), "{engine 2}%s", buffer);
+ CProcessVariables(buffer, sizeof(buffer));
+ Format(sBufferTag, sizeof(sBufferTag), "{prefix}%s", tag);
+ CProcessVariables(sBufferTag, sizeof(sBufferTag));
+ CAddWhiteSpace(buffer, sizeof(buffer));
+ CAddWhiteSpace(sBufferTag, sizeof(sBufferTag));
+
+ ShowActivityEx(client, sBufferTag, " %s", buffer);
+}
+
+/*
+* Strips all colors from the specified string.
+*
+* msg - String buffer.
+* size - Size of the string.
+*
+* Return - N/A
+*/
+stock void CRemoveColors(char[] msg, int size)
+{
+ CProcessVariables(msg, size, true);
+}
+
+/*
+* Processes colors in a string by replacing found tags with color/hex codes.
+*
+* msg - String buffer.
+* size - Size of the string.
+* removecolors - Whether to remove colors or keep them. (same as CRemoveColors)
+*
+* Return - N/A
+*/
+stock void CProcessVariables(char[] msg, int size, bool removecolors = false)
+{
+ Init();
+
+ char[] sOut = new char[size]; char[] sCode = new char[size]; char[] color = new char[size];
+ int iOutPos = 0; int iCodePos = -1;
+ int iMsgLen = strlen(msg);
+
+ for (int i = 0; i < iMsgLen; i++)
+ {
+ if (msg[i] == '{')
+ iCodePos = 0;
+
+ if (iCodePos > -1)
+ {
+ sCode[iCodePos] = msg[i];
+ sCode[iCodePos + 1] = '\0';
+
+ if (msg[i] == '}' || i == iMsgLen - 1)
+ {
+ strcopy(sCode, strlen(sCode) - 1, sCode[1]);
+ StringToLower(sCode);
+
+ if (CGetColor(sCode, color, size))
+ {
+ if (!removecolors)
+ {
+ StrCat(sOut, size, color);
+ iOutPos += strlen(color);
+ }
+ }
+ else
+ {
+ Format(sOut, size, "%s{%s}", sOut, sCode);
+ iOutPos += strlen(sCode) + 2;
+ }
+
+ iCodePos = -1;
+ strcopy(sCode, size, "");
+ strcopy(color, size, "");
+ }
+ else
+ iCodePos++;
+
+ continue;
+ }
+
+ sOut[iOutPos] = msg[i];
+ iOutPos++;
+ sOut[iOutPos] = '\0';
+ }
+
+ strcopy(msg, size, sOut);
+}
+
+/*
+* Retrieves the color/hex code for a specified color name.
+*
+* name - Color to search for.
+* color - String buffer.
+* size - Size of the string.
+*
+* Return - True if found, false otherwise.
+*/
+stock bool CGetColor(const char[] name, char[] color, int size)
+{
+ if (name[0] == '\0')
+ return false;
+
+ if (name[0] == '@')
+ {
+ int iSpace;
+ char sData[64]; char m_sName[64];
+ strcopy(m_sName, sizeof(m_sName), name[1]);
+
+ if ((iSpace = FindCharInString(m_sName, ' ')) != -1 && (iSpace + 1 < strlen(m_sName)))
+ {
+ strcopy(m_sName, iSpace + 1, m_sName);
+ strcopy(sData, sizeof(sData), m_sName[iSpace + 1]);
+ }
+
+ if (color[0] != '\0')
+ return true;
+ }
+ else if (name[0] == '#')
+ {
+ if (strlen(name) == 7)
+ {
+ Format(color, size, "\x07%s", name[1]);
+ return true;
+ }
+
+ if (strlen(name) == 9)
+ {
+ Format(color, size, "\x08%s", name[1]);
+ return true;
+ }
+ }
+ else if (StrContains(name, "player ", false) == 0 && strlen(name) > 7)
+ {
+ int client = StringToInt(name[7]);
+
+ if (client < 1 || client > MaxClients || !IsClientInGame(client))
+ {
+ strcopy(color, size, "\x01");
+ LogError("Invalid client index %d", client);
+ return false;
+ }
+
+ strcopy(color, size, "\x01");
+
+ switch (GetClientTeam(client))
+ {
+ case 1: g_hColors.GetString("engine 8", color, size);
+ case 2: g_hColors.GetString("engine 9", color, size);
+ case 3: g_hColors.GetString("engine 11", color, size);
+ }
+
+ return true;
+ }
+ else
+ return g_hColors.GetString(name, color, size);
+
+ return false;
+}
+
+/*
+* Checks if the specified color exists.
+*
+* name - Color to search for.
+*
+* Return - True if found, false otherwise.
+*/
+stock bool CExistColor(const char[] name)
+{
+ if (name[0] == '\0' || name[0] == '@' || name[0] == '#')
+ return false;
+
+ char color[64];
+ return g_hColors.GetString(name, color, sizeof(color));
+}
+
+/*
+* Sends a raw SayText2 usermsg to the specified client with settings.
+*
+* client - Client index.
+* message - Message string.
+* author - Author index.
+* chat - "0 - raw text, 1 - sets CHAT_FILTER_PUBLICCHAT "
+*
+* Return - N/A
+*/
+stock void CSayText2(int client, const char[] message, int author, bool chat = true)
+{
+ if (client < 1 || client > MaxClients)
+ return;
+
+ Handle hMsg = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS);
+ if (GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf)
+ {
+ PbSetInt(hMsg, "ent_idx", author);
+ PbSetBool(hMsg, "chat", chat);
+ PbSetString(hMsg, "msg_name", message);
+ PbAddString(hMsg, "params", "");
+ PbAddString(hMsg, "params", "");
+ PbAddString(hMsg, "params", "");
+ PbAddString(hMsg, "params", "");
+ }
+ else
+ {
+ BfWriteByte(hMsg, author);
+ BfWriteByte(hMsg, true);
+ BfWriteString(hMsg, message);
+ }
+
+ EndMessage();
+}
+
+/*
+* Adds a space to the start a string buffer.
+*
+* buffer - String buffer.
+* size - Size of the string.
+*
+* Return - N/A
+*/
+stock void CAddWhiteSpace(char[] buffer, int size)
+{
+ if (!IsSource2009())
+ Format(buffer, size, " %s", buffer);
+}
+
+// ----------------------------------------------------------------------------------------
+// Private stuff
+// ----------------------------------------------------------------------------------------
+
+stock bool Init()
+{
+ if (g_bInit)
+ {
+ LoadColors();
+ return true;
+ }
+
+ for (int i = 1; i <= MaxClients; i++)
+ g_bSkipPlayers[i] = false;
+
+ LoadColors();
+ g_bInit = true;
+
+ return true;
+}
+
+stock void LoadColors()
+{
+ if (g_hColors == null)
+ g_hColors = new StringMap();
+ else
+ g_hColors.Clear();
+
+ g_hColors.SetString("default", "\x01");
+ g_hColors.SetString("teamcolor", "\x03");
+
+ if (IsSource2009())
+ {
+ g_hColors.SetString("aliceblue", "\x07F0F8FF");
+ g_hColors.SetString("allies", "\x074D7942");
+ g_hColors.SetString("ancient", "\x07EB4B4B");
+ g_hColors.SetString("antiquewhite", "\x07FAEBD7");
+ g_hColors.SetString("aqua", "\x0700FFFF");
+ g_hColors.SetString("aquamarine", "\x077FFFD4");
+ g_hColors.SetString("arcana", "\x07ADE55C");
+ g_hColors.SetString("axis", "\x07FF4040");
+ g_hColors.SetString("azure", "\x07007FFF");
+ g_hColors.SetString("beige", "\x07F5F5DC");
+ g_hColors.SetString("bisque", "\x07FFE4C4");
+ g_hColors.SetString("black", "\x07000000");
+ g_hColors.SetString("blanchedalmond", "\x07FFEBCD");
+ g_hColors.SetString("blue", "\x0799CCFF");
+ g_hColors.SetString("blueviolet", "\x078A2BE2");
+ g_hColors.SetString("brown", "\x07A52A2A");
+ g_hColors.SetString("burlywood", "\x07DEB887");
+ g_hColors.SetString("cadetblue", "\x075F9EA0");
+ g_hColors.SetString("chartreuse", "\x077FFF00");
+ g_hColors.SetString("chocolate", "\x07D2691E");
+ g_hColors.SetString("collectors", "\x07AA0000");
+ g_hColors.SetString("common", "\x07B0C3D9");
+ g_hColors.SetString("community", "\x0770B04A");
+ g_hColors.SetString("coral", "\x07FF7F50");
+ g_hColors.SetString("cornflowerblue", "\x076495ED");
+ g_hColors.SetString("cornsilk", "\x07FFF8DC");
+ g_hColors.SetString("corrupted", "\x07A32C2E");
+ g_hColors.SetString("crimson", "\x07DC143C");
+ g_hColors.SetString("cyan", "\x0700FFFF");
+ g_hColors.SetString("darkblue", "\x0700008B");
+ g_hColors.SetString("darkcyan", "\x07008B8B");
+ g_hColors.SetString("darkgoldenrod", "\x07B8860B");
+ g_hColors.SetString("darkgray", "\x07A9A9A9");
+ g_hColors.SetString("darkgrey", "\x07A9A9A9");
+ g_hColors.SetString("darkgreen", "\x07006400");
+ g_hColors.SetString("darkkhaki", "\x07BDB76B");
+ g_hColors.SetString("darkmagenta", "\x078B008B");
+ g_hColors.SetString("darkolivegreen", "\x07556B2F");
+ g_hColors.SetString("darkorange", "\x07FF8C00");
+ g_hColors.SetString("darkorchid", "\x079932CC");
+ g_hColors.SetString("darkred", "\x078B0000");
+ g_hColors.SetString("darksalmon", "\x07E9967A");
+ g_hColors.SetString("darkseagreen", "\x078FBC8F");
+ g_hColors.SetString("darkslateblue", "\x07483D8B");
+ g_hColors.SetString("darkslategray", "\x072F4F4F");
+ g_hColors.SetString("darkslategrey", "\x072F4F4F");
+ g_hColors.SetString("darkturquoise", "\x0700CED1");
+ g_hColors.SetString("darkviolet", "\x079400D3");
+ g_hColors.SetString("deeppink", "\x07FF1493");
+ g_hColors.SetString("deepskyblue", "\x0700BFFF");
+ g_hColors.SetString("dimgray", "\x07696969");
+ g_hColors.SetString("dimgrey", "\x07696969");
+ g_hColors.SetString("dodgerblue", "\x071E90FF");
+ g_hColors.SetString("exalted", "\x07CCCCCD");
+ g_hColors.SetString("firebrick", "\x07B22222");
+ g_hColors.SetString("floralwhite", "\x07FFFAF0");
+ g_hColors.SetString("forestgreen", "\x07228B22");
+ g_hColors.SetString("frozen", "\x074983B3");
+ g_hColors.SetString("fuchsia", "\x07FF00FF");
+ g_hColors.SetString("fullblue", "\x070000FF");
+ g_hColors.SetString("fullred", "\x07FF0000");
+ g_hColors.SetString("gainsboro", "\x07DCDCDC");
+ g_hColors.SetString("genuine", "\x074D7455");
+ g_hColors.SetString("ghostwhite", "\x07F8F8FF");
+ g_hColors.SetString("gold", "\x07FFD700");
+ g_hColors.SetString("goldenrod", "\x07DAA520");
+ g_hColors.SetString("gray", "\x07CCCCCC");
+ g_hColors.SetString("grey", "\x07CCCCCC");
+ g_hColors.SetString("green", "\x073EFF3E");
+ g_hColors.SetString("greenyellow", "\x07ADFF2F");
+ g_hColors.SetString("haunted", "\x0738F3AB");
+ g_hColors.SetString("honeydew", "\x07F0FFF0");
+ g_hColors.SetString("hotpink", "\x07FF69B4");
+ g_hColors.SetString("immortal", "\x07E4AE33");
+ g_hColors.SetString("indianred", "\x07CD5C5C");
+ g_hColors.SetString("indigo", "\x074B0082");
+ g_hColors.SetString("ivory", "\x07FFFFF0");
+ g_hColors.SetString("khaki", "\x07F0E68C");
+ g_hColors.SetString("lavender", "\x07E6E6FA");
+ g_hColors.SetString("lavenderblush", "\x07FFF0F5");
+ g_hColors.SetString("lawngreen", "\x077CFC00");
+ g_hColors.SetString("legendary", "\x07D32CE6");
+ g_hColors.SetString("lemonchiffon", "\x07FFFACD");
+ g_hColors.SetString("lightblue", "\x07ADD8E6");
+ g_hColors.SetString("lightcoral", "\x07F08080");
+ g_hColors.SetString("lightcyan", "\x07E0FFFF");
+ g_hColors.SetString("lightgoldenrodyellow", "\x07FAFAD2");
+ g_hColors.SetString("lightgray", "\x07D3D3D3");
+ g_hColors.SetString("lightgrey", "\x07D3D3D3");
+ g_hColors.SetString("lightgreen", "\x0799FF99");
+ g_hColors.SetString("lightpink", "\x07FFB6C1");
+ g_hColors.SetString("lightsalmon", "\x07FFA07A");
+ g_hColors.SetString("lightseagreen", "\x0720B2AA");
+ g_hColors.SetString("lightskyblue", "\x0787CEFA");
+ g_hColors.SetString("lightslategray", "\x07778899");
+ g_hColors.SetString("lightslategrey", "\x07778899");
+ g_hColors.SetString("lightsteelblue", "\x07B0C4DE");
+ g_hColors.SetString("lightyellow", "\x07FFFFE0");
+ g_hColors.SetString("lime", "\x0700FF00");
+ g_hColors.SetString("limegreen", "\x0732CD32");
+ g_hColors.SetString("linen", "\x07FAF0E6");
+ g_hColors.SetString("magenta", "\x07FF00FF");
+ g_hColors.SetString("maroon", "\x07800000");
+ g_hColors.SetString("mediumaquamarine", "\x0766CDAA");
+ g_hColors.SetString("mediumblue", "\x070000CD");
+ g_hColors.SetString("mediumorchid", "\x07BA55D3");
+ g_hColors.SetString("mediumpurple", "\x079370D8");
+ g_hColors.SetString("mediumseagreen", "\x073CB371");
+ g_hColors.SetString("mediumslateblue", "\x077B68EE");
+ g_hColors.SetString("mediumspringgreen", "\x0700FA9A");
+ g_hColors.SetString("mediumturquoise", "\x0748D1CC");
+ g_hColors.SetString("mediumvioletred", "\x07C71585");
+ g_hColors.SetString("midnightblue", "\x07191970");
+ g_hColors.SetString("mintcream", "\x07F5FFFA");
+ g_hColors.SetString("mistyrose", "\x07FFE4E1");
+ g_hColors.SetString("moccasin", "\x07FFE4B5");
+ g_hColors.SetString("mythical", "\x078847FF");
+ g_hColors.SetString("navajowhite", "\x07FFDEAD");
+ g_hColors.SetString("navy", "\x07000080");
+ g_hColors.SetString("normal", "\x07B2B2B2");
+ g_hColors.SetString("oldlace", "\x07FDF5E6");
+ g_hColors.SetString("olive", "\x079EC34F");
+ g_hColors.SetString("olivedrab", "\x076B8E23");
+ g_hColors.SetString("orange", "\x07FFA500");
+ g_hColors.SetString("orangered", "\x07FF4500");
+ g_hColors.SetString("orchid", "\x07DA70D6");
+ g_hColors.SetString("palegoldenrod", "\x07EEE8AA");
+ g_hColors.SetString("palegreen", "\x0798FB98");
+ g_hColors.SetString("paleturquoise", "\x07AFEEEE");
+ g_hColors.SetString("palevioletred", "\x07D87093");
+ g_hColors.SetString("papayawhip", "\x07FFEFD5");
+ g_hColors.SetString("peachpuff", "\x07FFDAB9");
+ g_hColors.SetString("peru", "\x07CD853F");
+ g_hColors.SetString("pink", "\x07FFC0CB");
+ g_hColors.SetString("plum", "\x07DDA0DD");
+ g_hColors.SetString("powderblue", "\x07B0E0E6");
+ g_hColors.SetString("purple", "\x07800080");
+ g_hColors.SetString("rare", "\x074B69FF");
+ g_hColors.SetString("red", "\x07FF4040");
+ g_hColors.SetString("rosybrown", "\x07BC8F8F");
+ g_hColors.SetString("royalblue", "\x074169E1");
+ g_hColors.SetString("saddlebrown", "\x078B4513");
+ g_hColors.SetString("salmon", "\x07FA8072");
+ g_hColors.SetString("sandybrown", "\x07F4A460");
+ g_hColors.SetString("seagreen", "\x072E8B57");
+ g_hColors.SetString("seashell", "\x07FFF5EE");
+ g_hColors.SetString("selfmade", "\x0770B04A");
+ g_hColors.SetString("sienna", "\x07A0522D");
+ g_hColors.SetString("silver", "\x07C0C0C0");
+ g_hColors.SetString("skyblue", "\x0787CEEB");
+ g_hColors.SetString("slateblue", "\x076A5ACD");
+ g_hColors.SetString("slategray", "\x07708090");
+ g_hColors.SetString("slategrey", "\x07708090");
+ g_hColors.SetString("snow", "\x07FFFAFA");
+ g_hColors.SetString("springgreen", "\x0700FF7F");
+ g_hColors.SetString("steelblue", "\x074682B4");
+ g_hColors.SetString("strange", "\x07CF6A32");
+ g_hColors.SetString("tan", "\x07D2B48C");
+ g_hColors.SetString("teal", "\x07008080");
+ g_hColors.SetString("thistle", "\x07D8BFD8");
+ g_hColors.SetString("tomato", "\x07FF6347");
+ g_hColors.SetString("turquoise", "\x0740E0D0");
+ g_hColors.SetString("uncommon", "\x07B0C3D9");
+ g_hColors.SetString("unique", "\x07FFD700");
+ g_hColors.SetString("unusual", "\x078650AC");
+ g_hColors.SetString("valve", "\x07A50F79");
+ g_hColors.SetString("vintage", "\x07476291");
+ g_hColors.SetString("violet", "\x07EE82EE");
+ g_hColors.SetString("wheat", "\x07F5DEB3");
+ g_hColors.SetString("white", "\x07FFFFFF");
+ g_hColors.SetString("whitesmoke", "\x07F5F5F5");
+ g_hColors.SetString("yellow", "\x07FFFF00");
+ g_hColors.SetString("yellowgreen", "\x079ACD32");
+ }
+ else
+ {
+ g_hColors.SetString("red", "\x07");
+ g_hColors.SetString("lightred", "\x0F");
+ g_hColors.SetString("darkred", "\x02");
+ g_hColors.SetString("bluegrey", "\x0A");
+ g_hColors.SetString("blue", "\x0B");
+ g_hColors.SetString("darkblue", "\x0C");
+ g_hColors.SetString("purple", "\x03");
+ g_hColors.SetString("orchid", "\x0E");
+ g_hColors.SetString("yellow", "\x09");
+ g_hColors.SetString("gold", "\x10");
+ g_hColors.SetString("lightgreen", "\x05");
+ g_hColors.SetString("green", "\x04");
+ g_hColors.SetString("lime", "\x06");
+ g_hColors.SetString("grey", "\x08");
+ g_hColors.SetString("grey2", "\x0D");
+ }
+
+ g_hColors.SetString("engine 1", "\x01");
+ g_hColors.SetString("engine 2", "\x02");
+ g_hColors.SetString("engine 3", "\x03");
+ g_hColors.SetString("engine 4", "\x04");
+ g_hColors.SetString("engine 5", "\x05");
+ g_hColors.SetString("engine 6", "\x06");
+ g_hColors.SetString("engine 7", "\x07");
+ g_hColors.SetString("engine 8", "\x08");
+ g_hColors.SetString("engine 9", "\x09");
+ g_hColors.SetString("engine 10", "\x0A");
+ g_hColors.SetString("engine 11", "\x0B");
+ g_hColors.SetString("engine 12", "\x0C");
+ g_hColors.SetString("engine 13", "\x0D");
+ g_hColors.SetString("engine 14", "\x0E");
+ g_hColors.SetString("engine 15", "\x0F");
+ g_hColors.SetString("engine 16", "\x10");
+}
+
+stock bool HasBrackets(const char[] sSource)
+{
+ return (sSource[0] == '{' && sSource[strlen(sSource) - 1] == '}');
+}
+
+stock void StringToLower(char[] sSource)
+{
+ for (int i = 0; i < strlen(sSource); i++)
+ {
+ if (sSource[i] == '\0')
+ break;
+
+ sSource[i] = CharToLower(sSource[i]);
+ }
+}
+
+stock bool IsSource2009()
+{
+ EngineVersion iEngineVersion = GetEngineVersion();
+ return (iEngineVersion == Engine_CSS || iEngineVersion == Engine_TF2 || iEngineVersion == Engine_HL2DM || iEngineVersion == Engine_DODS);
+}
+
+stock void AddPrefixAndDefaultColor(char[] message, int size, char[] sDefaultColor = "engine 1", char[] sPrefixColor = "engine 2")
+{
+ if (g_sChatPrefix[0] != '\0' && !g_bIgnorePrefix)
+ Format(message, size, "{%s}[%s]{%s} %s", sPrefixColor, g_sChatPrefix, sDefaultColor, message);
+ else
+ Format(message, size, "{%s}%s", sDefaultColor, message);
+}
+
+stock void SendPlayerMessage(int client, char[] message, int author = 0)
+{
+ if (author > 0 && author <= MaxClients && IsClientInGame(author))
+ CSayText2(client, message, author);
+ else
+ PrintToChat(client, message);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/include/updater.inc b/sourcemod/scripting/include/updater.inc
new file mode 100644
index 0000000..f37bdf2
--- /dev/null
+++ b/sourcemod/scripting/include/updater.inc
@@ -0,0 +1,97 @@
+#if defined _updater_included
+ #endinput
+#endif
+#define _updater_included
+
+/**
+ * Adds your plugin to the updater. The URL will be updated if
+ * your plugin was previously added.
+ *
+ * @param url URL to your plugin's update file.
+ * @noreturn
+ */
+native Updater_AddPlugin(const String:url[]);
+
+/**
+ * Removes your plugin from the updater. This does not need to
+ * be called during OnPluginEnd.
+ *
+ * @noreturn
+ */
+native Updater_RemovePlugin();
+
+/**
+ * Forces your plugin to be checked for updates. The behaviour
+ * of the update is dependant on the server's configuration.
+ *
+ * @return True if an update was triggered. False otherwise.
+ * @error Plugin not found in updater.
+ */
+native bool:Updater_ForceUpdate();
+
+/**
+ * Called when your plugin is about to be checked for updates.
+ *
+ * @return Plugin_Handled to prevent checking, Plugin_Continue to allow it.
+ */
+forward Action:Updater_OnPluginChecking();
+
+/**
+ * Called when your plugin is about to begin downloading an available update.
+ *
+ * @return Plugin_Handled to prevent downloading, Plugin_Continue to allow it.
+ */
+forward Action:Updater_OnPluginDownloading();
+
+/**
+ * Called when your plugin's update files have been fully downloaded
+ * and are about to write to their proper location. This should be used
+ * to free read-only resources that require write access for your update.
+ *
+ * @note OnPluginUpdated will be called later during the same frame.
+ *
+ * @noreturn
+ */
+forward Updater_OnPluginUpdating();
+
+/**
+ * Called when your plugin's update has been completed. It is safe
+ * to reload your plugin at this time.
+ *
+ * @noreturn
+ */
+forward Updater_OnPluginUpdated();
+
+/**
+ * @brief Reloads a plugin.
+ *
+ * @param plugin Plugin Handle (INVALID_HANDLE uses the calling plugin).
+ * @noreturn
+ */
+stock ReloadPlugin(Handle:plugin=INVALID_HANDLE)
+{
+ decl String:filename[64];
+ GetPluginFilename(plugin, filename, sizeof(filename));
+ ServerCommand("sm plugins reload %s", filename);
+}
+
+
+public SharedPlugin:__pl_updater =
+{
+ name = "updater",
+ file = "updater.smx",
+#if defined REQUIRE_PLUGIN
+ required = 1,
+#else
+ required = 0,
+#endif
+};
+
+#if !defined REQUIRE_PLUGIN
+public __pl_updater_SetNTVOptional()
+{
+ MarkNativeAsOptional("Updater_AddPlugin");
+ MarkNativeAsOptional("Updater_RemovePlugin");
+ MarkNativeAsOptional("Updater_ForceUpdate");
+}
+#endif
diff --git a/sourcemod/scripting/momsurffix/baseplayer.sp b/sourcemod/scripting/momsurffix/baseplayer.sp
new file mode 100644
index 0000000..c638773
--- /dev/null
+++ b/sourcemod/scripting/momsurffix/baseplayer.sp
@@ -0,0 +1,189 @@
+#define MAX_EDICT_BITS 11
+#define NUM_ENT_ENTRY_BITS (MAX_EDICT_BITS + 1)
+#define NUM_ENT_ENTRIES (1 << NUM_ENT_ENTRY_BITS)
+#define ENT_ENTRY_MASK (NUM_ENT_ENTRIES - 1)
+#define INVALID_EHANDLE_INDEX 0xFFFFFFFF
+
+enum struct CBasePlayerOffsets
+{
+ //...
+ int m_surfaceFriction;
+ //...
+ int m_hGroundEntity;
+ //...
+ int m_MoveType;
+ //...
+}
+
+enum struct CBaseHandleOffsets
+{
+ int m_Index;
+}
+
+enum struct CEntInfoOffsets
+{
+ int m_pEntity;
+ int m_SerialNumber;
+ //...
+ int size;
+}
+
+enum struct CBaseEntityListOffsets
+{
+ int m_EntPtrArray;
+}
+
+enum struct BasePlayerOffsets
+{
+ CBasePlayerOffsets cbpoffsets;
+ CBaseHandleOffsets cbhoffsets;
+ CEntInfoOffsets ceioffsets;
+ CBaseEntityListOffsets cbeloffsets;
+}
+static BasePlayerOffsets offsets;
+
+methodmap CBasePlayer < AddressBase
+{
+ property float m_surfaceFriction
+ {
+ public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cbpoffsets.m_surfaceFriction, NumberType_Int32)); }
+ }
+
+ //...
+
+ property Address m_hGroundEntity
+ {
+ public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.cbpoffsets.m_hGroundEntity, NumberType_Int32)); }
+ }
+
+ //...
+
+ property MoveType m_MoveType
+ {
+ public get() { return view_as<MoveType>(LoadFromAddress(this.Address + offsets.cbpoffsets.m_MoveType, NumberType_Int8)); }
+ }
+}
+
+methodmap CBaseEntityList < AddressBase
+{
+ property PseudoStackArray m_EntPtrArray
+ {
+ public get() { return view_as<PseudoStackArray>(LoadFromAddress(this.Address + offsets.cbeloffsets.m_EntPtrArray, NumberType_Int32)); }
+ }
+}
+
+static CBaseEntityList g_pEntityList;
+
+methodmap CBaseHandle < AddressBase
+{
+ property int m_Index
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cbhoffsets.m_Index, NumberType_Int32); }
+ }
+
+ public CBaseHandle Get()
+ {
+ return LookupEntity(this);
+ }
+
+ public int GetEntryIndex()
+ {
+ return
+ }
+}
+
+methodmap CEntInfo < AddressBase
+{
+ public static int Size()
+ {
+ return offsets.ceioffsets.size;
+ }
+
+ property CBaseHandle m_pEntity
+ {
+ public get() { return view_as<CBaseHandle>(LoadFromAddress(this.Address + offsets.ceioffsets.m_pEntity, NumberType_Int32)); }
+ }
+
+ property int m_SerialNumber
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.ceioffsets.m_SerialNumber, NumberType_Int32); }
+ }
+}
+
+stock bool InitBasePlayer(GameData gd)
+{
+ char buff[128];
+ bool early = false;
+
+ if(gEngineVersion == Engine_CSS)
+ {
+ //g_pEntityList
+ g_pEntityList = view_as<CBaseEntityList>(gd.GetAddress("g_pEntityList"));
+ ASSERT_MSG(g_pEntityList.Address != Address_Null, "Can't get \"g_pEntityList\" address from gamedata. Gamedata needs an update.");
+
+ //CBaseEntityList
+ ASSERT_FMT(gd.GetKeyValue("CBaseEntityList::m_EntPtrArray", buff, sizeof(buff)), "Can't get \"CBaseEntityList::m_EntPtrArray\" offset from gamedata.");
+ offsets.cbeloffsets.m_EntPtrArray = StringToInt(buff);
+
+ //CEntInfo
+ ASSERT_FMT(gd.GetKeyValue("CEntInfo::m_pEntity", buff, sizeof(buff)), "Can't get \"CEntInfo::m_pEntity\" offset from gamedata.");
+ offsets.ceioffsets.m_pEntity = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CEntInfo::m_SerialNumber", buff, sizeof(buff)), "Can't get \"CEntInfo::m_SerialNumber\" offset from gamedata.");
+ offsets.ceioffsets.m_SerialNumber = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CEntInfo::size", buff, sizeof(buff)), "Can't get \"CEntInfo::size\" offset from gamedata.");
+ offsets.ceioffsets.size = StringToInt(buff);
+
+ //CBaseHandle
+ ASSERT_FMT(gd.GetKeyValue("CBaseHandle::m_Index", buff, sizeof(buff)), "Can't get \"CBaseHandle::m_Index\" offset from gamedata.");
+ offsets.cbhoffsets.m_Index = StringToInt(buff);
+
+ //CBasePlayer
+ ASSERT_FMT(gd.GetKeyValue("CBasePlayer::m_surfaceFriction", buff, sizeof(buff)), "Can't get \"CBasePlayer::m_surfaceFriction\" offset from gamedata.");
+ int offs = StringToInt(buff);
+ int prop_offs = FindSendPropInfo("CBasePlayer", "m_szLastPlaceName");
+ ASSERT_FMT(prop_offs > 0, "Can't get \"CBasePlayer::m_szLastPlaceName\" offset from FindSendPropInfo().");
+ offsets.cbpoffsets.m_surfaceFriction = prop_offs + offs;
+ }
+ else if(gEngineVersion == Engine_CSGO)
+ {
+ //CBasePlayer
+ ASSERT_FMT(gd.GetKeyValue("CBasePlayer::m_surfaceFriction", buff, sizeof(buff)), "Can't get \"CBasePlayer::m_surfaceFriction\" offset from gamedata.");
+ int offs = StringToInt(buff);
+ int prop_offs = FindSendPropInfo("CBasePlayer", "m_ubEFNoInterpParity");
+ ASSERT_FMT(prop_offs > 0, "Can't get \"CBasePlayer::m_ubEFNoInterpParity\" offset from FindSendPropInfo().");
+ offsets.cbpoffsets.m_surfaceFriction = prop_offs - offs;
+ }
+
+ offsets.cbpoffsets.m_hGroundEntity = FindSendPropInfo("CBasePlayer", "m_hGroundEntity");
+ ASSERT_FMT(offsets.cbpoffsets.m_hGroundEntity > 0, "Can't get \"CBasePlayer::m_hGroundEntity\" offset from FindSendPropInfo().");
+
+ if(IsValidEntity(0))
+ {
+ offsets.cbpoffsets.m_MoveType = FindDataMapInfo(0, "m_MoveType");
+ ASSERT_FMT(offsets.cbpoffsets.m_MoveType != -1, "Can't get \"CBasePlayer::m_MoveType\" offset from FindDataMapInfo().");
+ }
+ else
+ early = true;
+
+ return early;
+}
+
+stock void LateInitBasePlayer(GameData gd)
+{
+ ASSERT(IsValidEntity(0));
+ offsets.cbpoffsets.m_MoveType = FindDataMapInfo(0, "m_MoveType");
+ ASSERT_FMT(offsets.cbpoffsets.m_MoveType != -1, "Can't get \"CBasePlayer::m_MoveType\" offset from FindDataMapInfo().");
+}
+
+stock CBaseHandle LookupEntity(CBaseHandle handle)
+{
+ if(handle.m_Index == INVALID_EHANDLE_INDEX)
+ return view_as<CBaseHandle>(0);
+
+ CEntInfo pInfo = view_as<CEntInfo>(g_pEntityList.m_EntPtrArray.Get32(handle.m_Index & ENT_ENTRY_MASK, CEntInfo.Size()));
+
+ if(pInfo.m_SerialNumber == (handle.m_Index >> NUM_ENT_ENTRY_BITS))
+ return pInfo.m_pEntity;
+ else
+ return view_as<CBaseHandle>(0);
+} \ No newline at end of file
diff --git a/sourcemod/scripting/momsurffix/gamemovement.sp b/sourcemod/scripting/momsurffix/gamemovement.sp
new file mode 100644
index 0000000..a51beeb
--- /dev/null
+++ b/sourcemod/scripting/momsurffix/gamemovement.sp
@@ -0,0 +1,411 @@
+enum struct CGameMovementOffsets
+{
+ int player;
+ int mv;
+ //...
+ int m_pTraceListData;
+ int m_nTraceCount;
+}
+
+enum struct CMoveDataOffsets
+{
+ int m_nPlayerHandle;
+ //...
+ int m_vecVelocity;
+ //...
+ int m_vecAbsOrigin;
+}
+
+enum struct GameMoventOffsets
+{
+ CGameMovementOffsets cgmoffsets;
+ CMoveDataOffsets cmdoffsets;
+}
+static GameMoventOffsets offsets;
+
+methodmap CMoveData < AddressBase
+{
+ property CBaseHandle m_nPlayerHandle
+ {
+ public get() { return view_as<CBaseHandle>(this.Address + offsets.cmdoffsets.m_nPlayerHandle); }
+ }
+
+ //...
+
+ property Vector m_vecVelocity
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.cmdoffsets.m_vecVelocity); }
+ }
+
+ //...
+
+ property Vector m_vecAbsOrigin
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.cmdoffsets.m_vecAbsOrigin); }
+ }
+}
+
+methodmap CGameMovement < AddressBase
+{
+ property CBasePlayer player
+ {
+ public get() { return view_as<CBasePlayer>(LoadFromAddress(this.Address + offsets.cgmoffsets.player, NumberType_Int32)); }
+ }
+
+ property CMoveData mv
+ {
+ public get() { return view_as<CMoveData>(LoadFromAddress(this.Address + offsets.cgmoffsets.mv, NumberType_Int32)); }
+ }
+
+ //...
+
+ property ITraceListData m_pTraceListData
+ {
+ public get() { return view_as<ITraceListData>(LoadFromAddress(this.Address + offsets.cgmoffsets.m_pTraceListData, NumberType_Int32)); }
+ }
+
+ property int m_nTraceCount
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cgmoffsets.m_nTraceCount, NumberType_Int32); }
+ public set(int _tracecount) { StoreToAddress(this.Address + offsets.cgmoffsets.m_nTraceCount, _tracecount, NumberType_Int32, false); }
+ }
+}
+
+static Handle gAddToTouched;
+
+methodmap IMoveHelper < AddressBase
+{
+ public bool AddToTouched(CGameTrace trace, Vector vec)
+ {
+ return SDKCall(gAddToTouched, this.Address, trace, vec);
+ }
+}
+
+enum //Collision_Group_t
+{
+ COLLISION_GROUP_NONE = 0,
+ COLLISION_GROUP_DEBRIS, // Collides with nothing but world and static stuff
+ COLLISION_GROUP_DEBRIS_TRIGGER, // Same as debris, but hits triggers
+ COLLISION_GROUP_INTERACTIVE_DEBRIS, // Collides with everything except other interactive debris or debris
+ COLLISION_GROUP_INTERACTIVE, // Collides with everything except interactive debris or debris
+ COLLISION_GROUP_PLAYER,
+ COLLISION_GROUP_BREAKABLE_GLASS,
+ COLLISION_GROUP_VEHICLE,
+ COLLISION_GROUP_PLAYER_MOVEMENT, // For HL2, same as Collision_Group_Player, for
+ // TF2, this filters out other players and CBaseObjects
+ COLLISION_GROUP_NPC, // Generic NPC group
+ COLLISION_GROUP_IN_VEHICLE, // for any entity inside a vehicle
+ COLLISION_GROUP_WEAPON, // for any weapons that need collision detection
+ COLLISION_GROUP_VEHICLE_CLIP, // vehicle clip brush to restrict vehicle movement
+ COLLISION_GROUP_PROJECTILE, // Projectiles!
+ COLLISION_GROUP_DOOR_BLOCKER, // Blocks entities not permitted to get near moving doors
+ COLLISION_GROUP_PASSABLE_DOOR, // Doors that the player shouldn't collide with
+ COLLISION_GROUP_DISSOLVING, // Things that are dissolving are in this group
+ COLLISION_GROUP_PUSHAWAY, // Nonsolid on client and server, pushaway in player code
+
+ COLLISION_GROUP_NPC_ACTOR, // Used so NPCs in scripts ignore the player.
+ COLLISION_GROUP_NPC_SCRIPTED, // USed for NPCs in scripts that should not collide with each other
+
+ LAST_SHARED_COLLISION_GROUP
+};
+
+static Handle gClipVelocity, gLockTraceFilter, gUnlockTraceFilter, gGetPlayerMins, gGetPlayerMaxs, gTracePlayerBBox;
+static IMoveHelper sm_pSingleton;
+
+stock void InitGameMovement(GameData gd)
+{
+ char buff[128];
+
+ //CGameMovement
+ ASSERT_FMT(gd.GetKeyValue("CGameMovement::player", buff, sizeof(buff)), "Can't get \"CGameMovement::player\" offset from gamedata.");
+ offsets.cgmoffsets.player = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameMovement::mv", buff, sizeof(buff)), "Can't get \"CGameMovement::mv\" offset from gamedata.");
+ offsets.cgmoffsets.mv = StringToInt(buff);
+
+ if(gEngineVersion == Engine_CSGO)
+ {
+ ASSERT_FMT(gd.GetKeyValue("CGameMovement::m_pTraceListData", buff, sizeof(buff)), "Can't get \"CGameMovement::m_pTraceListData\" offset from gamedata.");
+ offsets.cgmoffsets.m_pTraceListData = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameMovement::m_nTraceCount", buff, sizeof(buff)), "Can't get \"CGameMovement::m_nTraceCount\" offset from gamedata.");
+ offsets.cgmoffsets.m_nTraceCount = StringToInt(buff);
+ }
+
+ //CMoveData
+ if(gEngineVersion == Engine_CSS)
+ {
+ ASSERT_FMT(gd.GetKeyValue("CMoveData::m_nPlayerHandle", buff, sizeof(buff)), "Can't get \"CMoveData::m_nPlayerHandle\" offset from gamedata.");
+ offsets.cmdoffsets.m_nPlayerHandle = StringToInt(buff);
+ }
+
+ ASSERT_FMT(gd.GetKeyValue("CMoveData::m_vecVelocity", buff, sizeof(buff)), "Can't get \"CMoveData::m_vecVelocity\" offset from gamedata.");
+ offsets.cmdoffsets.m_vecVelocity = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CMoveData::m_vecAbsOrigin", buff, sizeof(buff)), "Can't get \"CMoveData::m_vecAbsOrigin\" offset from gamedata.");
+ offsets.cmdoffsets.m_vecAbsOrigin = StringToInt(buff);
+
+ if(gEngineVersion == Engine_CSGO)
+ {
+ //sm_pSingleton
+ sm_pSingleton = view_as<IMoveHelper>(gd.GetAddress("sm_pSingleton"));
+ ASSERT_MSG(sm_pSingleton.Address != Address_Null, "Can't get \"sm_pSingleton\" address from gamedata. Gamedata needs an update.");
+ }
+ else
+ {
+ //sm_pSingleton for late loading
+ sm_pSingleton = view_as<IMoveHelper>(gd.GetAddress("sm_pSingleton"));
+
+ //CMoveHelperServer::CMoveHelperServer
+ Handle dhook = DHookCreateDetour(Address_Null, CallConv_CDECL, ReturnType_Int, ThisPointer_Ignore);
+ ASSERT_MSG(DHookSetFromConf(dhook, gd, SDKConf_Signature, "CMoveHelperServer::CMoveHelperServer"), "Failed to get \"CMoveHelperServer::CMoveHelperServer\" signature. Gamedata needs an update.");
+ DHookAddParam(dhook, HookParamType_Int);
+ DHookEnableDetour(dhook, true, CMoveHelperServer_Dhook);
+ }
+
+ //AddToTouched
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("AddToTouched"));
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
+
+ gAddToTouched = EndPrepSDKCall();
+ ASSERT(gAddToTouched);
+
+ if(gEngineVersion == Engine_CSGO)
+ {
+ //ClipVelocity
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("ClipVelocity"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gClipVelocity = EndPrepSDKCall();
+ ASSERT(gClipVelocity);
+
+ //LockTraceFilter
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("LockTraceFilter"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gLockTraceFilter = EndPrepSDKCall();
+ ASSERT(gLockTraceFilter);
+
+ //UnlockTraceFilter
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("UnlockTraceFilter"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer);
+
+ gUnlockTraceFilter = EndPrepSDKCall();
+ ASSERT(gUnlockTraceFilter);
+ }
+ else if(gEngineVersion == Engine_CSS && gOSType == OSLinux)
+ {
+ //ClipVelocity
+ StartPrepSDKCall(SDKCall_Static);
+
+ ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CGameMovement::ClipVelocity"), "Failed to get \"CGameMovement::ClipVelocity\" signature. Gamedata needs an update.");
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gClipVelocity = EndPrepSDKCall();
+ ASSERT(gClipVelocity);
+ }
+
+ if(gEngineVersion == Engine_CSGO || gOSType == OSWindows)
+ {
+ //GetPlayerMins
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("GetPlayerMins"));
+
+ if(gEngineVersion == Engine_CSS)
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gGetPlayerMins = EndPrepSDKCall();
+ ASSERT(gGetPlayerMins);
+
+ //GetPlayerMaxs
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("GetPlayerMaxs"));
+
+ if(gEngineVersion == Engine_CSS)
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gGetPlayerMaxs = EndPrepSDKCall();
+ ASSERT(gGetPlayerMaxs);
+ }
+ else
+ {
+ //GetPlayerMins
+ StartPrepSDKCall(SDKCall_Static);
+
+ ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CGameMovement::GetPlayerMins"), "Failed to get \"CGameMovement::GetPlayerMins\" signature. Gamedata needs an update.");
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gGetPlayerMins = EndPrepSDKCall();
+ ASSERT(gGetPlayerMins);
+
+ //GetPlayerMaxs
+ StartPrepSDKCall(SDKCall_Static);
+
+ ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CGameMovement::GetPlayerMaxs"), "Failed to get \"CGameMovement::GetPlayerMaxs\" signature. Gamedata needs an update.");
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gGetPlayerMaxs = EndPrepSDKCall();
+ ASSERT(gGetPlayerMaxs);
+ }
+
+ //TracePlayerBBox
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("TracePlayerBBox"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gTracePlayerBBox = EndPrepSDKCall();
+ ASSERT(gTracePlayerBBox);
+}
+
+public MRESReturn CMoveHelperServer_Dhook(Handle hReturn, Handle hParams)
+{
+ if(sm_pSingleton.Address == Address_Null)
+ {
+ if(gOSType == OSLinux)
+ {
+ GameData gd = new GameData(GAME_DATA_FILE);
+
+ sm_pSingleton = view_as<IMoveHelper>(gd.GetAddress("sm_pSingleton"));
+ ASSERT_MSG(sm_pSingleton.Address != Address_Null, "Can't get \"sm_pSingleton\" address from gamedata. Gamedata needs an update.");
+
+ delete gd;
+ }
+ else
+ {
+ sm_pSingleton = view_as<IMoveHelper>(DHookGetReturn(hReturn));
+ ASSERT_MSG(sm_pSingleton.Address != Address_Null, "Can't get \"sm_pSingleton\" address from \"CMoveHelperServer::CMoveHelperServer\" dhook.");
+ }
+ }
+
+ return MRES_Ignored;
+}
+
+stock void TracePlayerBBox(CGameMovement pThis, Vector start, Vector end, int mask, int collisionGroup, CGameTrace trace)
+{
+ SDKCall(gTracePlayerBBox, pThis, start, end, mask, collisionGroup, trace);
+}
+
+stock CTraceFilterSimple LockTraceFilter(CGameMovement pThis, int collisionGroup)
+{
+ ASSERT(pThis.Address != Address_Null);
+ return SDKCall(gLockTraceFilter, pThis.Address, collisionGroup);
+}
+
+stock void UnlockTraceFilter(CGameMovement pThis, CTraceFilterSimple filter)
+{
+ ASSERT(pThis.Address != Address_Null);
+ SDKCall(gUnlockTraceFilter, pThis.Address, filter.Address);
+}
+
+stock int ClipVelocity(CGameMovement pThis, Vector invec, Vector normal, Vector out, float overbounce)
+{
+ if(gEngineVersion == Engine_CSGO)
+ {
+ ASSERT(pThis.Address != Address_Null);
+ return SDKCall(gClipVelocity, pThis.Address, invec.Address, normal.Address, out.Address, overbounce);
+ }
+ else if (gEngineVersion == Engine_CSS && gOSType == OSLinux)
+ {
+ return SDKCall(gClipVelocity, pThis.Address, invec.Address, normal.Address, out.Address, overbounce);
+ }
+ else
+ {
+ float backoff, angle, adjust;
+ int blocked;
+
+ angle = normal.z;
+
+ if(angle > 0.0)
+ blocked |= 0x01;
+ if(CloseEnoughFloat(angle, 0.0))
+ blocked |= 0x02;
+
+ backoff = invec.Dot(VectorToArray(normal)) * overbounce;
+
+ out.x = invec.x - (normal.x * backoff);
+ out.y = invec.y - (normal.y * backoff);
+ out.z = invec.z - (normal.z * backoff);
+
+ adjust = out.Dot(VectorToArray(normal));
+ if(adjust < 0.0)
+ {
+ out.x -= (normal.x * adjust);
+ out.y -= (normal.y * adjust);
+ out.z -= (normal.z * adjust);
+ }
+
+ return blocked;
+ }
+}
+
+stock Vector GetPlayerMinsCSS(CGameMovement pThis, Vector vec)
+{
+ if(gOSType == OSLinux)
+ {
+ SDKCall(gGetPlayerMins, vec.Address, pThis.Address);
+ return vec;
+ }
+ else
+ return SDKCall(gGetPlayerMins, pThis.Address, vec.Address);
+}
+
+stock Vector GetPlayerMaxsCSS(CGameMovement pThis, Vector vec)
+{
+ if(gOSType == OSLinux)
+ {
+ SDKCall(gGetPlayerMaxs, vec.Address, pThis.Address);
+ return vec;
+ }
+ else
+ return SDKCall(gGetPlayerMaxs, pThis.Address, vec.Address);
+}
+
+stock Vector GetPlayerMins(CGameMovement pThis)
+{
+ return SDKCall(gGetPlayerMins, pThis.Address);
+}
+
+stock Vector GetPlayerMaxs(CGameMovement pThis)
+{
+ return SDKCall(gGetPlayerMaxs, pThis.Address);
+}
+
+stock IMoveHelper MoveHelper()
+{
+ return sm_pSingleton;
+} \ No newline at end of file
diff --git a/sourcemod/scripting/momsurffix/gametrace.sp b/sourcemod/scripting/momsurffix/gametrace.sp
new file mode 100644
index 0000000..e8db1ad
--- /dev/null
+++ b/sourcemod/scripting/momsurffix/gametrace.sp
@@ -0,0 +1,480 @@
+enum struct cplane_tOffsets
+{
+ int normal;
+ int dist;
+ int type;
+ int signbits;
+}
+
+enum struct csurface_tOffsets
+{
+ int name;
+ int surfaceProps;
+ int flags;
+}
+
+enum struct CGameTraceOffsets
+{
+ //CBaseTrace
+ int startpos;
+ int endpos;
+ int plane;
+ int fraction;
+ int contents;
+ int dispFlags;
+ int allsolid;
+ int startsolid;
+ //CGameTrace
+ int fractionleftsolid;
+ int surface;
+ int hitgroup;
+ int physicsbone;
+ int m_pEnt;
+ int hitbox;
+ int size;
+}
+
+enum struct Ray_tOffsets
+{
+ int m_Start;
+ int m_Delta;
+ int m_StartOffset;
+ int m_Extents;
+ int m_pWorldAxisTransform;
+ int m_IsRay;
+ int m_IsSwept;
+ int size;
+}
+
+enum struct CTraceFilterSimpleOffsets
+{
+ int vptr;
+ int m_pPassEnt;
+ int m_collisionGroup;
+ int m_pExtraShouldHitCheckFunction;
+ int size;
+ Address vtable;
+}
+
+enum struct GameTraceOffsets
+{
+ cplane_tOffsets cptoffsets;
+ csurface_tOffsets cstoffsets;
+ CGameTraceOffsets cgtoffsets;
+ Ray_tOffsets rtoffsets;
+ CTraceFilterSimpleOffsets ctfsoffsets;
+}
+static GameTraceOffsets offsets;
+
+methodmap Cplane_t < AddressBase
+{
+ property Vector normal
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.cptoffsets.normal); }
+ }
+
+ property float dist
+ {
+ public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cptoffsets.dist, NumberType_Int32)); }
+ }
+
+ property char type
+ {
+ public get() { return view_as<char>(LoadFromAddress(this.Address + offsets.cptoffsets.type, NumberType_Int8)); }
+ }
+
+ property char signbits
+ {
+ public get() { return view_as<char>(LoadFromAddress(this.Address + offsets.cptoffsets.signbits, NumberType_Int8)); }
+ }
+}
+
+methodmap Csurface_t < AddressBase
+{
+ property Address name
+ {
+ public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.cstoffsets.name, NumberType_Int32)); }
+ }
+
+ property int surfaceProps
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cstoffsets.surfaceProps, NumberType_Int16); }
+ }
+
+ property int flags
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cstoffsets.flags, NumberType_Int16); }
+ }
+}
+
+methodmap CGameTrace < AllocatableBase
+{
+ public static int Size()
+ {
+ return offsets.cgtoffsets.size;
+ }
+
+ property Vector startpos
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.cgtoffsets.startpos); }
+ }
+
+ property Vector endpos
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.cgtoffsets.endpos); }
+ }
+
+ property Cplane_t plane
+ {
+ public get() { return view_as<Cplane_t>(this.Address + offsets.cgtoffsets.plane); }
+ }
+
+ property float fraction
+ {
+ public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cgtoffsets.fraction, NumberType_Int32)); }
+ }
+
+ property int contents
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.contents, NumberType_Int32); }
+ }
+
+ property int dispFlags
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.dispFlags, NumberType_Int16); }
+ }
+
+ property bool allsolid
+ {
+ public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.cgtoffsets.allsolid, NumberType_Int8)); }
+ }
+
+ property bool startsolid
+ {
+ public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.cgtoffsets.startsolid, NumberType_Int8)); }
+ }
+
+ property float fractionleftsolid
+ {
+ public get() { return view_as<float>(LoadFromAddress(this.Address + offsets.cgtoffsets.fractionleftsolid, NumberType_Int32)); }
+ }
+
+ property Csurface_t surface
+ {
+ public get() { return view_as<Csurface_t>(this.Address + offsets.cgtoffsets.surface); }
+ }
+
+ property int hitgroup
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.hitgroup, NumberType_Int32); }
+ }
+
+ property int physicsbone
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.physicsbone, NumberType_Int16); }
+ }
+
+ property Address m_pEnt
+ {
+ public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.cgtoffsets.m_pEnt, NumberType_Int32)); }
+ }
+
+ property int hitbox
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.cgtoffsets.hitbox, NumberType_Int32); }
+ }
+
+ public CGameTrace()
+ {
+ return MALLOC(CGameTrace);
+ }
+}
+
+methodmap Ray_t < AllocatableBase
+{
+ public static int Size()
+ {
+ return offsets.rtoffsets.size;
+ }
+
+ property Vector m_Start
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_Start); }
+ }
+
+ property Vector m_Delta
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_Delta); }
+ }
+
+ property Vector m_StartOffset
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_StartOffset); }
+ }
+
+ property Vector m_Extents
+ {
+ public get() { return view_as<Vector>(this.Address + offsets.rtoffsets.m_Extents); }
+ }
+
+ property Address m_pWorldAxisTransform
+ {
+ public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.rtoffsets.m_pWorldAxisTransform, NumberType_Int32)); }
+ public set(Address _worldaxistransform) { StoreToAddress(this.Address + offsets.rtoffsets.m_pWorldAxisTransform, view_as<int>(_worldaxistransform), NumberType_Int32, false); }
+ }
+
+ property bool m_IsRay
+ {
+ public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.rtoffsets.m_IsRay, NumberType_Int8)); }
+ public set(bool _isray) { StoreToAddress(this.Address + offsets.rtoffsets.m_IsRay, _isray, NumberType_Int8, false); }
+ }
+
+ property bool m_IsSwept
+ {
+ public get() { return view_as<bool>(LoadFromAddress(this.Address + offsets.rtoffsets.m_IsSwept, NumberType_Int8)); }
+ public set(bool _isswept) { StoreToAddress(this.Address + offsets.rtoffsets.m_IsSwept, _isswept, NumberType_Int8, false); }
+ }
+
+ public Ray_t()
+ {
+ return MALLOC(Ray_t);
+ }
+
+ //That function is quite heavy, linux builds have it inlined, so can't use it!
+ //Replacing this function with lighter alternative may increase speed by ~4 times!
+ //From my testings the main performance killer here is StoreToAddress()....
+ public void Init(float start[3], float end[3], float mins[3], float maxs[3])
+ {
+ float buff[3], buff2[3];
+
+ SubtractVectors(end, start, buff);
+ this.m_Delta.FromArray(buff);
+
+ if(gEngineVersion == Engine_CSGO)
+ this.m_pWorldAxisTransform = Address_Null;
+ this.m_IsSwept = (this.m_Delta.LengthSqr() != 0.0);
+
+ SubtractVectors(maxs, mins, buff);
+ ScaleVector(buff, 0.5);
+ this.m_Extents.FromArray(buff);
+
+ this.m_IsRay = (this.m_Extents.LengthSqr() < 1.0e-6);
+
+ AddVectors(mins, maxs, buff);
+ ScaleVector(buff, 0.5);
+ AddVectors(start, buff, buff2);
+ this.m_Start.FromArray(buff2);
+ NegateVector(buff);
+ this.m_StartOffset.FromArray(buff);
+ }
+}
+
+methodmap CTraceFilterSimple < AllocatableBase
+{
+ public static int Size()
+ {
+ return offsets.ctfsoffsets.size;
+ }
+
+ property Address vptr
+ {
+ public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.ctfsoffsets.vptr, NumberType_Int32)); }
+ public set(Address _vtbladdr) { StoreToAddress(this.Address + offsets.ctfsoffsets.vptr, view_as<int>(_vtbladdr), NumberType_Int32, false); }
+ }
+
+ property CBaseHandle m_pPassEnt
+ {
+ public get() { return view_as<CBaseHandle>(LoadFromAddress(this.Address + offsets.ctfsoffsets.m_pPassEnt, NumberType_Int32)); }
+ public set(CBaseHandle _passent) { StoreToAddress(this.Address + offsets.ctfsoffsets.m_pPassEnt, view_as<int>(_passent), NumberType_Int32, false); }
+ }
+
+ property int m_collisionGroup
+ {
+ public get() { return LoadFromAddress(this.Address + offsets.ctfsoffsets.m_collisionGroup, NumberType_Int32); }
+ public set(int _collisiongroup) { StoreToAddress(this.Address + offsets.ctfsoffsets.m_collisionGroup, _collisiongroup, NumberType_Int32, false); }
+ }
+
+ property Address m_pExtraShouldHitCheckFunction
+ {
+ public get() { return view_as<Address>(LoadFromAddress(this.Address + offsets.ctfsoffsets.m_pExtraShouldHitCheckFunction, NumberType_Int32)); }
+ public set(Address _checkfnc) { StoreToAddress(this.Address + offsets.ctfsoffsets.m_pExtraShouldHitCheckFunction, view_as<int>(_checkfnc), NumberType_Int32, false); }
+ }
+
+ public CTraceFilterSimple()
+ {
+ CTraceFilterSimple addr = MALLOC(CTraceFilterSimple);
+ addr.vptr = offsets.ctfsoffsets.vtable;
+ return addr;
+ }
+
+ public void Init(CBaseHandle passentity, int collisionGroup, Address pExtraShouldHitCheckFn = Address_Null)
+ {
+ this.m_pPassEnt = passentity;
+ this.m_collisionGroup = collisionGroup;
+ this.m_pExtraShouldHitCheckFunction = pExtraShouldHitCheckFn;
+ }
+}
+
+static Handle gCanTraceRay;
+
+methodmap ITraceListData < AddressBase
+{
+ public bool CanTraceRay(Ray_t ray)
+ {
+ return SDKCall(gCanTraceRay, this.Address, ray.Address);
+ }
+}
+
+static Handle gTraceRay, gTraceRayAgainstLeafAndEntityList;
+static Address gEngineTrace;
+
+stock void InitGameTrace(GameData gd)
+{
+ char buff[128];
+
+ //cplane_t
+ ASSERT_FMT(gd.GetKeyValue("cplane_t::normal", buff, sizeof(buff)), "Can't get \"cplane_t::normal\" offset from gamedata.");
+ offsets.cptoffsets.normal = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("cplane_t::dist", buff, sizeof(buff)), "Can't get \"cplane_t::dist\" offset from gamedata.");
+ offsets.cptoffsets.dist = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("cplane_t::type", buff, sizeof(buff)), "Can't get \"cplane_t::type\" offset from gamedata.");
+ offsets.cptoffsets.type = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("cplane_t::signbits", buff, sizeof(buff)), "Can't get \"cplane_t::signbits\" offset from gamedata.");
+ offsets.cptoffsets.signbits = StringToInt(buff);
+
+ //csurface_t
+ ASSERT_FMT(gd.GetKeyValue("csurface_t::name", buff, sizeof(buff)), "Can't get \"csurface_t::name\" offset from gamedata.");
+ offsets.cstoffsets.name = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("csurface_t::surfaceProps", buff, sizeof(buff)), "Can't get \"csurface_t::surfaceProps\" offset from gamedata.");
+ offsets.cstoffsets.surfaceProps = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("csurface_t::flags", buff, sizeof(buff)), "Can't get \"csurface_t::flags\" offset from gamedata.");
+ offsets.cstoffsets.flags = StringToInt(buff);
+
+ //CGameTrace
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::startpos", buff, sizeof(buff)), "Can't get \"CGameTrace::startpos\" offset from gamedata.");
+ offsets.cgtoffsets.startpos = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::endpos", buff, sizeof(buff)), "Can't get \"CGameTrace::endpos\" offset from gamedata.");
+ offsets.cgtoffsets.endpos = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::plane", buff, sizeof(buff)), "Can't get \"CGameTrace::plane\" offset from gamedata.");
+ offsets.cgtoffsets.plane = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::fraction", buff, sizeof(buff)), "Can't get \"CGameTrace::fraction\" offset from gamedata.");
+ offsets.cgtoffsets.fraction = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::contents", buff, sizeof(buff)), "Can't get \"CGameTrace::contents\" offset from gamedata.");
+ offsets.cgtoffsets.contents = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::dispFlags", buff, sizeof(buff)), "Can't get \"CGameTrace::dispFlags\" offset from gamedata.");
+ offsets.cgtoffsets.dispFlags = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::allsolid", buff, sizeof(buff)), "Can't get \"CGameTrace::allsolid\" offset from gamedata.");
+ offsets.cgtoffsets.allsolid = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::startsolid", buff, sizeof(buff)), "Can't get \"CGameTrace::startsolid\" offset from gamedata.");
+ offsets.cgtoffsets.startsolid = StringToInt(buff);
+
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::fractionleftsolid", buff, sizeof(buff)), "Can't get \"CGameTrace::fractionleftsolid\" offset from gamedata.");
+ offsets.cgtoffsets.fractionleftsolid = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::surface", buff, sizeof(buff)), "Can't get \"CGameTrace::surface\" offset from gamedata.");
+ offsets.cgtoffsets.surface = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::hitgroup", buff, sizeof(buff)), "Can't get \"CGameTrace::hitgroup\" offset from gamedata.");
+ offsets.cgtoffsets.hitgroup = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::physicsbone", buff, sizeof(buff)), "Can't get \"CGameTrace::physicsbone\" offset from gamedata.");
+ offsets.cgtoffsets.physicsbone = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::m_pEnt", buff, sizeof(buff)), "Can't get \"CGameTrace::m_pEnt\" offset from gamedata.");
+ offsets.cgtoffsets.m_pEnt = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::hitbox", buff, sizeof(buff)), "Can't get \"CGameTrace::hitbox\" offset from gamedata.");
+ offsets.cgtoffsets.hitbox = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CGameTrace::size", buff, sizeof(buff)), "Can't get \"CGameTrace::size\" offset from gamedata.");
+ offsets.cgtoffsets.size = StringToInt(buff);
+
+ //Ray_t
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_Start", buff, sizeof(buff)), "Can't get \"Ray_t::m_Start\" offset from gamedata.");
+ offsets.rtoffsets.m_Start = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_Delta", buff, sizeof(buff)), "Can't get \"Ray_t::m_Delta\" offset from gamedata.");
+ offsets.rtoffsets.m_Delta = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_StartOffset", buff, sizeof(buff)), "Can't get \"Ray_t::m_StartOffset\" offset from gamedata.");
+ offsets.rtoffsets.m_StartOffset = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_Extents", buff, sizeof(buff)), "Can't get \"Ray_t::m_Extents\" offset from gamedata.");
+ offsets.rtoffsets.m_Extents = StringToInt(buff);
+
+ if(gEngineVersion == Engine_CSGO)
+ {
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_pWorldAxisTransform", buff, sizeof(buff)), "Can't get \"Ray_t::m_pWorldAxisTransform\" offset from gamedata.");
+ offsets.rtoffsets.m_pWorldAxisTransform = StringToInt(buff);
+ }
+
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_IsRay", buff, sizeof(buff)), "Can't get \"Ray_t::m_IsRay\" offset from gamedata.");
+ offsets.rtoffsets.m_IsRay = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::m_IsSwept", buff, sizeof(buff)), "Can't get \"Ray_t::m_IsSwept\" offset from gamedata.");
+ offsets.rtoffsets.m_IsSwept = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("Ray_t::size", buff, sizeof(buff)), "Can't get \"Ray_t::size\" offset from gamedata.");
+ offsets.rtoffsets.size = StringToInt(buff);
+
+ //CTraceFilterSimple
+ ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::vptr", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::vptr\" offset from gamedata.");
+ offsets.ctfsoffsets.vptr = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::m_pPassEnt", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::m_pPassEnt\" offset from gamedata.");
+ offsets.ctfsoffsets.m_pPassEnt = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::m_collisionGroup", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::m_collisionGroup\" offset from gamedata.");
+ offsets.ctfsoffsets.m_collisionGroup = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::m_pExtraShouldHitCheckFunction", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::m_pExtraShouldHitCheckFunction\" offset from gamedata.");
+ offsets.ctfsoffsets.m_pExtraShouldHitCheckFunction = StringToInt(buff);
+ ASSERT_FMT(gd.GetKeyValue("CTraceFilterSimple::size", buff, sizeof(buff)), "Can't get \"CTraceFilterSimple::size\" offset from gamedata.");
+ offsets.ctfsoffsets.size = StringToInt(buff);
+
+ if(gEngineVersion == Engine_CSS)
+ {
+ offsets.ctfsoffsets.vtable = gd.GetAddress("CTraceFilterSimple::vtable");
+ ASSERT_MSG(offsets.ctfsoffsets.vtable != Address_Null, "Can't get \"CTraceFilterSimple::vtable\" address from gamedata. Gamedata needs an update.");
+ }
+
+ //enginetrace
+ gd.GetKeyValue("CEngineTrace", buff, sizeof(buff));
+ gEngineTrace = CreateInterface(buff);
+ ASSERT_MSG(gEngineTrace != Address_Null, "Can't create \"enginetrace\" from CreateInterface().");
+
+ //RayTrace
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("TraceRay"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gTraceRay = EndPrepSDKCall();
+ ASSERT(gTraceRay);
+
+ if(gEngineVersion == Engine_CSGO)
+ {
+ //TraceRayAgainstLeafAndEntityList
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("TraceRayAgainstLeafAndEntityList"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gTraceRayAgainstLeafAndEntityList = EndPrepSDKCall();
+ ASSERT(gTraceRayAgainstLeafAndEntityList);
+
+ //CanTraceRay
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("CanTraceRay"));
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain);
+
+ gCanTraceRay = EndPrepSDKCall();
+ ASSERT(gCanTraceRay);
+ }
+}
+
+stock void TraceRayAgainstLeafAndEntityList(Ray_t ray, ITraceListData traceData, int mask, CTraceFilterSimple filter, CGameTrace trace)
+{
+ SDKCall(gTraceRayAgainstLeafAndEntityList, gEngineTrace, ray.Address, traceData.Address, mask, filter.Address, trace.Address);
+}
+
+stock void TraceRay(Ray_t ray, int mask, CTraceFilterSimple filter, CGameTrace trace)
+{
+ SDKCall(gTraceRay, gEngineTrace, ray.Address, mask, filter.Address, trace.Address);
+}
diff --git a/sourcemod/scripting/momsurffix/utils.sp b/sourcemod/scripting/momsurffix/utils.sp
new file mode 100644
index 0000000..e68e342
--- /dev/null
+++ b/sourcemod/scripting/momsurffix/utils.sp
@@ -0,0 +1,279 @@
+#define MALLOC(%1) view_as<%1>(AllocatableBase._malloc(%1.Size(), #%1))
+#define MEMORYPOOL_NAME_MAX 128
+
+enum struct MemoryPoolEntry
+{
+ Address addr;
+ char name[MEMORYPOOL_NAME_MAX];
+}
+
+methodmap AllocatableBase < AddressBase
+{
+ public static Address _malloc(int size, const char[] name)
+ {
+ Address addr = Malloc(size, name);
+
+ return addr;
+ }
+
+ public void Free()
+ {
+ Free(this.Address);
+ }
+}
+
+methodmap PseudoStackArray < AddressBase
+{
+ public Address Get8(int idx, int size = 4)
+ {
+ ASSERT(idx >= 0);
+ ASSERT(size > 0);
+
+ return view_as<Address>(LoadFromAddress(this.Address + (idx * size), NumberType_Int8));
+ }
+
+ public Address Get16(int idx, int size = 4)
+ {
+ ASSERT(idx >= 0);
+ ASSERT(size > 0);
+
+ return view_as<Address>(LoadFromAddress(this.Address + (idx * size), NumberType_Int16));
+ }
+
+ public Address Get32(int idx, int size = 4)
+ {
+ ASSERT(idx >= 0);
+ ASSERT(size > 0);
+
+ return this.Address + (idx * size);
+ }
+}
+
+methodmap Vector < AllocatableBase
+{
+ public static int Size()
+ {
+ return 12;
+ }
+
+ public Vector()
+ {
+ return MALLOC(Vector);
+ }
+
+ property float x
+ {
+ public set(float _x) { StoreToAddress(this.Address, view_as<int>(_x), NumberType_Int32, false); }
+ public get() { return view_as<float>(LoadFromAddress(this.Address, NumberType_Int32)); }
+ }
+
+ property float y
+ {
+ public set(float _y) { StoreToAddress(this.Address + 4, view_as<int>(_y), NumberType_Int32, false); }
+ public get() { return view_as<float>(LoadFromAddress(this.Address + 4, NumberType_Int32)); }
+ }
+
+ property float z
+ {
+ public set(float _z) { StoreToAddress(this.Address + 8, view_as<int>(_z), NumberType_Int32, false); }
+ public get() { return view_as<float>(LoadFromAddress(this.Address + 8, NumberType_Int32)); }
+ }
+
+ public void ToArray(float buff[3])
+ {
+ buff[0] = this.x;
+ buff[1] = this.y;
+ buff[2] = this.z;
+ }
+
+ public void FromArray(float buff[3])
+ {
+ this.x = buff[0];
+ this.y = buff[1];
+ this.z = buff[2];
+ }
+
+ public void CopyTo(Vector dst)
+ {
+ dst.x = this.x;
+ dst.y = this.y;
+ dst.z = this.z;
+ }
+
+ public float LengthSqr()
+ {
+ return this.x*this.x + this.y*this.y + this.z*this.z;
+ }
+
+ public float Length()
+ {
+ return SquareRoot(this.LengthSqr());
+ }
+
+ public float Dot(float vec[3])
+ {
+ return this.x*vec[0] + this.y*vec[1] + this.z*vec[2];
+ }
+}
+
+static Address g_pMemAlloc;
+static Handle gMalloc, gFree, gCreateInterface;
+static ArrayList gMemoryPool;
+
+stock void InitUtils(GameData gd)
+{
+ gMemoryPool = new ArrayList(sizeof(MemoryPoolEntry));
+
+ //CreateInterface
+ StartPrepSDKCall(SDKCall_Static);
+
+ ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "CreateInterface"), "Failed to get \"CreateInterface\" signature. Gamedata needs an update.");
+
+ PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gCreateInterface = EndPrepSDKCall();
+ ASSERT(gCreateInterface);
+
+ if(gEngineVersion == Engine_CSGO || gOSType == OSWindows)
+ {
+ //g_pMemAlloc
+ g_pMemAlloc = gd.GetAddress("g_pMemAlloc");
+ ASSERT_MSG(g_pMemAlloc != Address_Null, "Can't get \"g_pMemAlloc\" address from gamedata. Gamedata needs an update.");
+
+ //Malloc
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("Malloc"));
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gMalloc = EndPrepSDKCall();
+ ASSERT(gMalloc);
+
+ //Free
+ StartPrepSDKCall(SDKCall_Raw);
+
+ PrepSDKCall_SetVirtual(gd.GetOffset("Free"));
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gFree = EndPrepSDKCall();
+ ASSERT(gFree);
+ }
+ else
+ {
+ //Malloc
+ StartPrepSDKCall(SDKCall_Static);
+ ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "malloc"), "Failed to get \"malloc\" signature. Gamedata needs an update.");
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
+
+ gMalloc = EndPrepSDKCall();
+ ASSERT(gMalloc);
+
+ //Free
+ StartPrepSDKCall(SDKCall_Static);
+ ASSERT_MSG(PrepSDKCall_SetFromConf(gd, SDKConf_Signature, "free"), "Failed to get \"free\" signature. Gamedata needs an update.");
+
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+ PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
+
+ gFree = EndPrepSDKCall();
+ ASSERT(gFree);
+ }
+}
+
+stock Address CreateInterface(const char[] name)
+{
+ return SDKCall(gCreateInterface, name, 0);
+}
+
+stock Address Malloc(int size, const char[] name)
+{
+ ASSERT(gMemoryPool);
+ ASSERT(size > 0);
+
+ MemoryPoolEntry entry;
+ strcopy(entry.name, sizeof(MemoryPoolEntry::name), name);
+
+ if(gEngineVersion == Engine_CSS && gOSType == OSLinux)
+ entry.addr = SDKCall(gMalloc, 0, size);
+ else
+ entry.addr = SDKCall(gMalloc, g_pMemAlloc, size);
+
+ ASSERT_FMT(entry.addr != Address_Null, "Failed to allocate memory (size: %i)!", size);
+ gMemoryPool.PushArray(entry);
+
+ return entry.addr;
+}
+
+stock void Free(Address addr)
+{
+ ASSERT(addr != Address_Null);
+ ASSERT(gMemoryPool);
+ int idx = gMemoryPool.FindValue(addr, MemoryPoolEntry::addr);
+
+ //Memory wasn't allocated by this plugin, return.
+ if(idx == -1)
+ return;
+
+ gMemoryPool.Erase(idx);
+
+ if(gEngineVersion == Engine_CSS && gOSType == OSLinux)
+ SDKCall(gFree, 0, addr);
+ else
+ SDKCall(gFree, g_pMemAlloc, addr);
+}
+
+stock void AddToMemoryPool(Address addr, const char[] name)
+{
+ ASSERT(addr != Address_Null);
+ ASSERT(gMemoryPool);
+
+ MemoryPoolEntry entry;
+ strcopy(entry.name, sizeof(MemoryPoolEntry::name), name);
+ entry.addr = addr;
+
+ gMemoryPool.PushArray(entry);
+}
+
+stock void CleanUpUtils()
+{
+ if(!gMemoryPool)
+ return;
+
+ MemoryPoolEntry entry;
+
+ for(int i = 0; i < gMemoryPool.Length; i++)
+ {
+ gMemoryPool.GetArray(i, entry, sizeof(MemoryPoolEntry));
+ view_as<AllocatableBase>(entry.addr).Free();
+ }
+
+ delete gMemoryPool;
+}
+
+stock void DumpMemoryUsage()
+{
+ if(!gMemoryPool || (gMemoryPool && gMemoryPool.Length == 0))
+ {
+ PrintToServer(SNAME..."Theres's currently no active pool or it's empty!");
+ return;
+ }
+
+ MemoryPoolEntry entry;
+
+ PrintToServer(SNAME..."Active memory pool (%i):", gMemoryPool.Length);
+ for(int i = 0; i < gMemoryPool.Length; i++)
+ {
+ gMemoryPool.GetArray(i, entry, sizeof(MemoryPoolEntry));
+ PrintToServer(SNAME..."[%i]: 0x%08X \"%s\"", i, entry.addr, entry.name);
+ }
+} \ No newline at end of file