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