diff --git a/addons/sourcemod/scripting/gokz-core.sp b/addons/sourcemod/scripting/gokz-core.sp index 0adb978c..8ab9635b 100644 --- a/addons/sourcemod/scripting/gokz-core.sp +++ b/addons/sourcemod/scripting/gokz-core.sp @@ -343,6 +343,12 @@ public void OnMapStart() OnMapStart_FixMissingSpawns(); OnMapStart_Checkpoints(); OnMapStart_TeamNumber(); + OnMapStart_Demofix(); +} + +public void OnMapEnd() +{ + OnMapEnd_Demofix(); } public void OnGameFrame() diff --git a/addons/sourcemod/scripting/gokz-core/demofix.sp b/addons/sourcemod/scripting/gokz-core/demofix.sp index 55012f3a..fe8c83a1 100644 --- a/addons/sourcemod/scripting/gokz-core/demofix.sp +++ b/addons/sourcemod/scripting/gokz-core/demofix.sp @@ -1,11 +1,29 @@ 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() @@ -76,6 +94,10 @@ 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); diff --git a/addons/sourcemod/scripting/gokz-core/misc.sp b/addons/sourcemod/scripting/gokz-core/misc.sp index a00453e8..8ef86d2b 100644 --- a/addons/sourcemod/scripting/gokz-core/misc.sp +++ b/addons/sourcemod/scripting/gokz-core/misc.sp @@ -52,7 +52,7 @@ void ToggleNoclip(int client) void EnableNoclip(int client) { - if (IsPlayerAlive(client)) + if (IsValidClient(client) && IsPlayerAlive(client)) { Movement_SetMovetype(client, MOVETYPE_NOCLIP); GOKZ_StopTimer(client); @@ -61,7 +61,7 @@ void EnableNoclip(int client) void DisableNoclip(int client) { - if (IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP) + if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP) { noclipReleaseTime[client] = GetGameTickCount(); Movement_SetMovetype(client, MOVETYPE_WALK); @@ -86,7 +86,7 @@ void ToggleNoclipNotrigger(int client) void EnableNoclipNotrigger(int client) { - if (IsPlayerAlive(client)) + if (IsValidClient(client) && IsPlayerAlive(client)) { Movement_SetMovetype(client, MOVETYPE_NOCLIP); SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER); @@ -96,7 +96,7 @@ void EnableNoclipNotrigger(int client) void DisableNoclipNotrigger(int client) { - if (IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP) + if (IsValidClient(client) && IsPlayerAlive(client) && Movement_GetMovetype(client) == MOVETYPE_NOCLIP) { noclipReleaseTime[client] = GetGameTickCount(); Movement_SetMovetype(client, MOVETYPE_WALK); @@ -211,15 +211,29 @@ static bool savedOnLadder[MAXPLAYERS + 1]; void OnClientPutInServer_JoinTeam(int client) { - // After OnClientPutInServer, player is moved to the origin of a point_viewcontrol entity. - // We need to wait one tick before assign the player's team and teleport them to a valid spawn. - if (!IsFakeClient(client)) - { - RequestFrame(AutoJoinTeam, 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; @@ -238,10 +252,14 @@ void JoinTeam(int client, int newTeam, bool restorePos) if (newTeam == CS_TEAM_SPECTATOR && currentTeam != CS_TEAM_SPECTATOR) { - player.GetOrigin(savedOrigin[client]); - player.GetEyeAngles(savedAngles[client]); - savedOnLadder[client] = player.Movetype == MOVETYPE_LADDER; - hasSavedPosition[client] = true; + 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(); @@ -511,9 +529,3 @@ void OnMapStart_FixMissingSpawns() } } } - -static void AutoJoinTeam(int client) -{ - int team = GetRandomInt(CS_TEAM_T, CS_TEAM_CT); - JoinTeam(client, team, false); -} diff --git a/addons/sourcemod/scripting/gokz-core/natives.sp b/addons/sourcemod/scripting/gokz-core/natives.sp index ccef3a2d..9ae61555 100644 --- a/addons/sourcemod/scripting/gokz-core/natives.sp +++ b/addons/sourcemod/scripting/gokz-core/natives.sp @@ -469,7 +469,7 @@ public int Native_SetTeleportCount(Handle plugin, int numParams) public int Native_RegisterOption(Handle plugin, int numParams) { - char name[30]; + char name[GOKZ_OPTION_MAX_NAME_LENGTH]; GetNativeString(1, name, sizeof(name)); char description[255]; GetNativeString(2, description, sizeof(description)); @@ -478,7 +478,7 @@ public int Native_RegisterOption(Handle plugin, int numParams) public int Native_GetOptionProp(Handle plugin, int numParams) { - char option[30]; + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; GetNativeString(1, option, sizeof(option)); OptionProp prop = GetNativeCell(2); any value = GetOptionProp(option, prop); @@ -494,7 +494,7 @@ public int Native_GetOptionProp(Handle plugin, int numParams) public int Native_SetOptionProp(Handle plugin, int numParams) { - char option[30]; + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; GetNativeString(1, option, sizeof(option)); OptionProp prop = GetNativeCell(2); return SetOptionProp(option, prop, GetNativeCell(3)); @@ -502,14 +502,14 @@ public int Native_SetOptionProp(Handle plugin, int numParams) public int Native_GetOption(Handle plugin, int numParams) { - char option[30]; + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; GetNativeString(2, option, sizeof(option)); return view_as(GetOption(GetNativeCell(1), option)); } public int Native_SetOption(Handle plugin, int numParams) { - char option[30]; + char option[GOKZ_OPTION_MAX_NAME_LENGTH]; GetNativeString(2, option, sizeof(option)); return view_as(SetOption(GetNativeCell(1), option, GetNativeCell(3))); } diff --git a/addons/sourcemod/scripting/gokz-global.sp b/addons/sourcemod/scripting/gokz-global.sp index 8881e768..82abe48b 100644 --- a/addons/sourcemod/scripting/gokz-global.sp +++ b/addons/sourcemod/scripting/gokz-global.sp @@ -41,7 +41,7 @@ bool gB_APIKeyCheck; bool gB_ModeCheck[MODE_COUNT]; bool gB_BannedCommandsCheck; char gC_CurrentMap[64]; -char gC_CurrentMapPath[PLATFORM_MAX_PATH]; +int gI_CurrentMapFileSize; bool gB_InValidRun[MAXPLAYERS + 1]; bool gB_GloballyVerified[MAXPLAYERS + 1]; bool gB_EnforcerOnFreshMap; @@ -50,7 +50,7 @@ int gI_FPSMax[MAXPLAYERS + 1]; bool gB_waitingForFPSKick[MAXPLAYERS + 1]; bool gB_MapValidated; int gI_MapID; -int gI_MapFilesize; +int gI_MapFileSize; int gI_MapTier; ConVar gCV_gokz_settings_enforcer; @@ -89,7 +89,7 @@ public void OnPluginStart() gB_APIKeyCheck = false; gB_MapValidated = false; gI_MapID = -1; - gI_MapFilesize = -1; + gI_MapFileSize = -1; gI_MapTier = -1; for (int mode = 0; mode < MODE_COUNT; mode++) @@ -194,7 +194,7 @@ public void FPSCheck(QueryCookie cookie, int client, ConVarQueryResult result, c if (!gB_waitingForFPSKick[client]) { gB_waitingForFPSKick[client] = true; - CreateTimer(GL_FPS_MAX_KICK_TIMEOUT, FPSKickPlayer, client, TIMER_FLAG_NO_MAPCHANGE); + 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)) { @@ -221,8 +221,9 @@ public void MYAWCheck(QueryCookie cookie, int client, ConVarQueryResult result, } } -Action FPSKickPlayer(Handle timer, int 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); @@ -310,6 +311,9 @@ 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 @@ -381,7 +385,7 @@ bool MapCheck() { return gB_MapValidated && gI_MapID > 0 - && gI_MapFilesize == FileSize(gC_CurrentMapPath); + && gI_MapFileSize == gI_CurrentMapFileSize; } void PrintGlobalCheckToChat(int client) @@ -562,10 +566,6 @@ public void OnEnforcedConVarChanged(ConVar convar, const char[] oldValue, const static void SetupAPI() { - GetCurrentMap(gC_CurrentMap, sizeof(gC_CurrentMap)); - GetMapDisplayName(gC_CurrentMap, gC_CurrentMap, sizeof(gC_CurrentMap)); - GetMapFullPath(gC_CurrentMapPath, sizeof(gC_CurrentMapPath)); - GlobalAPI_GetAuthStatus(GetAuthStatusCallback); GlobalAPI_GetModes(GetModeInfoCallback); GlobalAPI_GetMapByName(GetMapCallback, _, gC_CurrentMap); @@ -645,7 +645,7 @@ public int GetMapCallback(JSON_Object map_json, GlobalAPIRequestData request) gB_MapValidated = map.IsValidated; gI_MapID = map.Id; - gI_MapFilesize = map.Filesize; + gI_MapFileSize = map.Filesize; gI_MapTier = map.Difficulty; // We don't do that earlier cause we need the map ID @@ -708,4 +708,4 @@ static void LoadSounds() 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/addons/sourcemod/scripting/gokz-global/api.sp b/addons/sourcemod/scripting/gokz-global/api.sp index 47d32ea4..6f1a7c39 100644 --- a/addons/sourcemod/scripting/gokz-global/api.sp +++ b/addons/sourcemod/scripting/gokz-global/api.sp @@ -45,6 +45,11 @@ void CreateNatives() 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) @@ -107,3 +112,28 @@ 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)); } + +public int Native_GetAPIKeyValid(Handle plugin, int numParams) +{ + return view_as(gB_APIKeyCheck); +} + +public int Native_GetPluginsValid(Handle plugin, int numParams) +{ + return view_as(gB_BannedCommandsCheck); +} + +public int Native_GetSettingsEnforcerValid(Handle plugin, int numParams) +{ + return view_as(gCV_gokz_settings_enforcer.BoolValue && gB_EnforcerOnFreshMap); +} + +public int Native_GetMapValid(Handle plugin, int numParams) +{ + return view_as(MapCheck()); +} + +public int Native_GetPlayerValid(Handle plugin, int numParams) +{ + return view_as(gB_GloballyVerified[GetNativeCell(1)]); +} diff --git a/addons/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp b/addons/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp index b0e2ff79..ee576d6b 100644 --- a/addons/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp +++ b/addons/sourcemod/scripting/gokz-jumpstats/jump_tracking.sp @@ -286,7 +286,15 @@ enum struct JumpTracker } else { - return JumpType_LadderJump; + // Check for ladder gliding. + if (GetClientButtons(this.jumper) & IN_JUMP) + { + return JumpType_Invalid; + } + else + { + return JumpType_LadderJump; + } } } else if (!jumped) diff --git a/addons/sourcemod/scripting/gokz-localranks/db/js_top.sp b/addons/sourcemod/scripting/gokz-localranks/db/js_top.sp index 1f4a50a9..37528e12 100644 --- a/addons/sourcemod/scripting/gokz-localranks/db/js_top.sp +++ b/addons/sourcemod/scripting/gokz-localranks/db/js_top.sp @@ -269,7 +269,10 @@ public int MenuHandler_JumpTopList(Menu menu, MenuAction action, int param1, int 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 (GOKZ_RP_LoadJumpReplay(param1, path) == -1) + { + GOKZ_PrintToChat(param1, true, "%t", "No Replay for Jump"); + } } if (action == MenuAction_Cancel && param2 == MenuCancel_Exit) diff --git a/addons/sourcemod/scripting/gokz-replays.sp b/addons/sourcemod/scripting/gokz-replays.sp index f592d8bf..c8468545 100644 --- a/addons/sourcemod/scripting/gokz-replays.sp +++ b/addons/sourcemod/scripting/gokz-replays.sp @@ -37,7 +37,7 @@ public Plugin myinfo = bool gB_GOKZLocalDB; char gC_CurrentMap[64]; -int gC_CurrentMapFileSize; +int gI_CurrentMapFileSize; bool gB_HideNameChange; bool gB_NubRecordMissed[MAXPLAYERS + 1]; ArrayList g_ReplayInfoCache; @@ -190,8 +190,8 @@ public void OnConVarChanged(ConVar convar, const char[] oldValue, const char[] i public void OnClientPutInServer(int client) { - OnClientPutInServer_Recording(client); OnClientPutInServer_Playback(client); + OnClientPutInServer_Recording(client); } public void OnClientAuthorized(int client, const char[] auth) @@ -202,6 +202,7 @@ public void OnClientAuthorized(int client, const char[] auth) 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]) @@ -216,10 +217,21 @@ public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float 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_Recording(client); + GOKZ_OnTimerStart_Post_Recording(client); } public void GOKZ_OnTimerEnd_Post(int client, int course, float time, int teleportsUsed) @@ -285,11 +297,7 @@ static void HookEvents() static void UpdateCurrentMap() { GetCurrentMapDisplayName(gC_CurrentMap, sizeof(gC_CurrentMap)); - - char mapBuffer[PLATFORM_MAX_PATH]; - GetCurrentMap(mapBuffer, sizeof(mapBuffer)); - Format(mapBuffer, sizeof(mapBuffer), "maps/%s.bsp", mapBuffer); - gC_CurrentMapFileSize = FileSize(mapBuffer); + gI_CurrentMapFileSize = GetCurrentMapFileSize(); } diff --git a/addons/sourcemod/scripting/gokz-replays/playback.sp b/addons/sourcemod/scripting/gokz-replays/playback.sp index 5fb1e695..664126bc 100644 --- a/addons/sourcemod/scripting/gokz-replays/playback.sp +++ b/addons/sourcemod/scripting/gokz-replays/playback.sp @@ -25,11 +25,15 @@ 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]; @@ -307,7 +311,8 @@ static bool LoadPlayback(int client, int bot, char[] path) { if (!FileExists(path)) { - LogError("Failed to load file: \"%s\".", path); + // This can happen relatively frequently, e.g. for jumps without a replay, + // therefore we're not logging it for now to avoid clutter. return false; } @@ -331,7 +336,10 @@ static bool LoadPlayback(int client, int bot, char[] path) case 1: { botReplayVersion[bot] = 1; - LoadFormatVersion1Replay(file, bot); + if (!LoadFormatVersion1Replay(file, bot)) + { + return false; + } } case 2: { @@ -353,7 +361,7 @@ static bool LoadPlayback(int client, int bot, char[] path) return true; } -static void LoadFormatVersion1Replay(File file, int bot) +static bool LoadFormatVersion1Replay(File file, int bot) { // Old replays only support runs, not jumps botReplayType[bot] = ReplayType_Run; @@ -377,6 +385,10 @@ static void LoadFormatVersion1Replay(File file, int 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); @@ -416,6 +428,14 @@ static void LoadFormatVersion1Replay(File file, int bot) 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++) @@ -434,6 +454,7 @@ static void LoadFormatVersion1Replay(File file, int bot) botDataLoaded[bot] = true; delete file; + return true; } static bool LoadFormatVersion2Replay(File file, int client, int bot) @@ -514,6 +535,14 @@ static bool LoadFormatVersion2Replay(File file, int client, int bot) 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]); @@ -531,6 +560,7 @@ static bool LoadFormatVersion2Replay(File file, int client, int bot) int timeAsInt; file.ReadInt32(timeAsInt); botTime[bot] = view_as(timeAsInt); + botTimeTicks[bot] = RoundToNearest(botTime[bot] * tickrate); // Course file.ReadInt8(botCourse[bot]); @@ -559,16 +589,13 @@ static bool LoadFormatVersion2Replay(File file, int client, int bot) case ReplayType_Jump: { // Jump Type - int jumpType; - file.ReadInt8(jumpType); + file.ReadInt8(botJumpType[bot]); // Distance - float distance; - file.ReadInt32(view_as(distance)); + file.ReadInt32(view_as(botJumpDistance[bot])); // Block Distance - int blockDistance; - file.ReadInt32(blockDistance); + file.ReadInt32(botJumpBlockDistance[bot]); // Strafe Count int strafeCount; @@ -595,7 +622,7 @@ static bool LoadFormatVersion2Replay(File file, int client, int bot) // 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[jumpType], distance, blockDistance, strafeCount, sync, pre, max, airtime); + gC_JumpTypes[botJumpType[bot]], botJumpDistance[bot], botJumpBlockDistance[bot], strafeCount, sync, pre, max, airtime); } } @@ -895,7 +922,7 @@ void PlaybackVersion2(int client, int bot, int &buttons) EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)]); botCurrentTeleport[bot] = 0; } - if (playbackTick[bot] == playbackTickData[bot].Length - preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run) + if (playbackTick[bot] == botTimeTicks[bot] + preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run) { EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)]); } @@ -1092,28 +1119,13 @@ static void SetBotStuff(int bot) GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]); GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]); - // Set bot clan tag - char tag[MAX_NAME_LENGTH]; - if (botCourse[bot] == 0) - { // Main course so tag "MODE NUB/PRO" - FormatEx(tag, sizeof(tag), "%s %s", - gC_ModeNamesShort[botMode[bot]], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]); - } - else - { // Bonus course so tag "MODE B# NUB/PRO" - FormatEx(tag, sizeof(tag), "%s B%d %s", - gC_ModeNamesShort[botMode[bot]], botCourse[bot], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]); - } - CS_SetClientClanTag(client, tag); - - // Set bot name e.g. "DanZay (01:23.45)" - char name[MAX_NAME_LENGTH]; - FormatEx(name, sizeof(name), "%s (%s)", botAlias[bot], GOKZ_FormatTime(botTime[bot])); - gB_HideNameChange = true; - SetClientName(client, name); - + // Clan tag and name + SetBotClanTag(bot); + SetBotName(bot); + // Set the bot's team based on if it's NUB or PRO - if (GOKZ_GetTimeTypeEx(botTeleportsUsed[bot]) == TimeType_Pro) + if (botReplayType[bot] == ReplayType_Run + && GOKZ_GetTimeTypeEx(botTeleportsUsed[bot]) == TimeType_Pro) { GOKZ_JoinTeam(client, CS_TEAM_CT); } @@ -1121,7 +1133,7 @@ static void SetBotStuff(int bot) { GOKZ_JoinTeam(client, CS_TEAM_T); } - + // Set bot weapons // Always start by removing the pistol and knife int currentPistol = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); @@ -1165,6 +1177,77 @@ static void SetBotStuff(int bot) 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() { diff --git a/addons/sourcemod/scripting/gokz-replays/recording.sp b/addons/sourcemod/scripting/gokz-replays/recording.sp index e12a8c9f..e686adfc 100644 --- a/addons/sourcemod/scripting/gokz-replays/recording.sp +++ b/addons/sourcemod/scripting/gokz-replays/recording.sp @@ -24,8 +24,10 @@ static bool timerRunning[MAXPLAYERS + 1]; static bool recordingPaused[MAXPLAYERS + 1]; static bool postRunRecording[MAXPLAYERS + 1]; static ArrayList recordedRecentData[MAXPLAYERS + 1]; -static ArrayList recordedPostRunData[MAXPLAYERS + 1]; static ArrayList recordedRunData[MAXPLAYERS + 1]; +static ArrayList recordedPostRunData[MAXPLAYERS + 1]; +static Handle runningRunBreatherTimer[MAXPLAYERS + 1]; +static ArrayList runningJumpstatTimers[MAXPLAYERS + 1]; // =====[ EVENTS ]===== @@ -39,10 +41,7 @@ void OnMapStart_Recording() void OnClientPutInServer_Recording(int client) { - recordedRecentData[client] = new ArrayList(sizeof(ReplayTickData)); - recordedRunData[client] = new ArrayList(sizeof(ReplayTickData)); - recordedPostRunData[client] = new ArrayList(sizeof(ReplayTickData)); - recordingIndex[client] = 0; + ClearClientRecordingState(client); } void OnClientAuthorized_Recording(int client) @@ -65,6 +64,37 @@ void OnClientAuthorized_Recording(int client) } } +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]) @@ -97,12 +127,12 @@ void OnPlayerRunCmdPost_Recording(int client, int buttons, int tickCount, const recordedRunData[client].Resize(runTick + 1); recordedRunData[client].SetArray(runTick, tickData); } - + if (postRunRecording[client]) { - int runTick = GetArraySize(recordedPostRunData[client]); - recordedPostRunData[client].Resize(runTick + 1); - recordedPostRunData[client].SetArray(runTick, tickData); + int tick = GetArraySize(recordedPostRunData[client]); + recordedPostRunData[client].Resize(tick + 1); + recordedPostRunData[client].SetArray(tick, tickData); } int tick = recordingIndex[client]; @@ -119,7 +149,19 @@ void OnPlayerRunCmdPost_Recording(int client, int buttons, int tickCount, const recordedRecentData[client].SetArray(tick, tickData); } -void GOKZ_OnTimerStart_Recording(int client) +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) { timerRunning[client] = true; StartRunRecording(client); @@ -127,37 +169,66 @@ void GOKZ_OnTimerStart_Recording(int client) void GOKZ_OnTimerEnd_Recording(int client, int course, float time, int teleportsUsed) { - DataPack dp = new DataPack(); - dp.WriteCell(client); - dp.WriteCell(course); - dp.WriteFloat(time); - dp.WriteCell(teleportsUsed); - delete recordedPostRunData[client]; + if (!timerRunning[client]) + { + return; + } + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteFloat(time); + data.WriteCell(teleportsUsed); + + // 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); + } + + timerRunning[client] = false; + 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] = new ArrayList(sizeof(ReplayTickData)); - if (timerRunning[client]) + recordedRunData[client] = tmp; + recordedRunData[client].Clear(); + + runningRunBreatherTimer[client] = CreateTimer(RP_PLAYBACK_BREATHER_TIME, Timer_EndRecording, data); + if (runningRunBreatherTimer[client] == INVALID_HANDLE) { - CreateTimer(RP_PLAYBACK_BREATHER_TIME, EndRecording, dp); - postRunRecording[client] = true; - timerRunning[client] = false; + LogError("Could not create a timer so can't end the run replay recording"); } } -public Action EndRecording(Handle timer, DataPack dp) +public Action Timer_EndRecording(Handle timer, DataPack data) { - dp.Reset(); - int client = dp.ReadCell(); - int course = dp.ReadCell(); - float time = dp.ReadFloat(); - int teleportsUsed = dp.ReadCell(); - delete dp; - + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + float time = data.ReadFloat(); + int teleportsUsed = 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)) { Call_OnTimerEnd_Post(client, "", course, time, teleportsUsed); - return; + return Plugin_Stop; } char path[PLATFORM_MAX_PATH]; @@ -169,6 +240,8 @@ public Action EndRecording(Handle timer, DataPack dp) { Call_OnTimerEnd_Post(client, "", course, time, teleportsUsed); } + + return Plugin_Stop; } void GOKZ_OnPause_Recording(int client) @@ -222,40 +295,89 @@ void GOKZ_AC_OnPlayerSuspected_Recording(int client, ACReason 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 dp = new DataPack(); - dp.WriteCell(client); - dp.WriteCell(jumptype); - dp.WriteFloat(distance); - dp.WriteCell(block); - dp.WriteCell(strafes); - dp.WriteFloat(sync); - dp.WriteFloat(pre); - dp.WriteFloat(max); - dp.WriteCell(airtime); - CreateTimer(RP_PLAYBACK_BREATHER_TIME, SaveJump, dp); -} - -public Action SaveJump(Handle timer, DataPack dp) -{ - dp.Reset(); - int client = dp.ReadCell(); - int jumptype = dp.ReadCell(); - float distance = dp.ReadFloat(); - int block = dp.ReadCell(); - int strafes = dp.ReadCell(); - float sync = dp.ReadFloat(); - float pre = dp.ReadFloat(); - float max = dp.ReadFloat(); - int airtime = dp.ReadCell(); - delete dp; + 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; + timerRunning[client] = false; + 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)) @@ -263,14 +385,6 @@ static void StartRunRecording(int client) return; } - // *Very* ugly hack to fix an exception when starting the timer on the very - // first tick after loading the plugin. - if (recordedRecentData[client].Length == 0) - { - RequestFrame(StartRunRecording, client); - return; - } - QueryClientConVar(client, "sensitivity", SensitivityCheck, client); QueryClientConVar(client, "m_yaw", MYAWCheck, client); @@ -467,7 +581,7 @@ static void FillGeneralHeader(GeneralReplayHeader generalHeader, int client, int generalHeader.replayType = replayType; generalHeader.gokzVersion = GOKZ_VERSION; generalHeader.mapName = gC_CurrentMap; - generalHeader.mapFileSize = gC_CurrentMapFileSize; + generalHeader.mapFileSize = gI_CurrentMapFileSize; generalHeader.serverIP = FindConVar("hostip").IntValue; generalHeader.timestamp = GetTime(); GetClientName(client, generalHeader.playerAlias, sizeof(GeneralReplayHeader::playerAlias)); @@ -535,19 +649,16 @@ static void WriteTickData(File file, int client, int replayType, int airtime = 0 { ReplayTickData tickData; ReplayTickData prevTickData; - int previousI = 0; bool isFirstTick = true; switch(replayType) { case ReplayType_Run: { - // Full run including pre and post for (int i = 0; i < recordedPostRunData[client].Length; i++) { recordedPostRunData[client].GetArray(i, tickData); - recordedPostRunData[client].GetArray(previousI, prevTickData); + recordedPostRunData[client].GetArray(IntMax(0, i-1), prevTickData); WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); - previousI = i; isFirstTick = false; } } @@ -557,9 +668,8 @@ static void WriteTickData(File file, int client, int replayType, int airtime = 0 { int rollingI = RecordingIndexAdd(client, i); recordedRecentData[client].GetArray(rollingI, tickData); - recordedRecentData[client].GetArray(previousI, prevTickData); + recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData); WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); - previousI = i; isFirstTick = false; } @@ -571,9 +681,8 @@ static void WriteTickData(File file, int client, int replayType, int airtime = 0 { int rollingI = RecordingIndexAdd(client, i - replayLength); recordedRecentData[client].GetArray(rollingI, tickData); - recordedRecentData[client].GetArray(previousI, prevTickData); + recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData); WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); - previousI = i; isFirstTick = false; } } @@ -805,3 +914,12 @@ static int RecordingIndexAdd(int client, int offset) } 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/addons/sourcemod/scripting/include/gokz.inc b/addons/sourcemod/scripting/include/gokz.inc index d6df9e70..f1087a51 100644 --- a/addons/sourcemod/scripting/include/gokz.inc +++ b/addons/sourcemod/scripting/include/gokz.inc @@ -670,6 +670,17 @@ stock void GetCurrentMapDisplayName(char[] buffer, int maxlength) 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. * diff --git a/addons/sourcemod/scripting/include/gokz/core.inc b/addons/sourcemod/scripting/include/gokz/core.inc index a39e8012..dc025e74 100644 --- a/addons/sourcemod/scripting/include/gokz/core.inc +++ b/addons/sourcemod/scripting/include/gokz/core.inc @@ -259,7 +259,7 @@ enum TriggerType #define GOKZ_CFG_OPTIONS_DESCRIPTION "description" #define GOKZ_CFG_OPTIONS_DEFAULT "default" -#define GOKZ_OPTION_MAX_NAME_LENGTH 50 +#define GOKZ_OPTION_MAX_NAME_LENGTH 30 #define GOKZ_OPTION_MAX_DESC_LENGTH 255 #define GENERAL_OPTION_CATEGORY "General" diff --git a/addons/sourcemod/scripting/include/gokz/global.inc b/addons/sourcemod/scripting/include/gokz/global.inc index db4d350c..01480574 100644 --- a/addons/sourcemod/scripting/include/gokz/global.inc +++ b/addons/sourcemod/scripting/include/gokz/global.inc @@ -187,6 +187,42 @@ native void GOKZ_GL_GetFinishes(int client, int mode, int timeType); */ 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 ]===== @@ -265,6 +301,12 @@ 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"); diff --git a/addons/sourcemod/scripting/include/gokz/jumpstats.inc b/addons/sourcemod/scripting/include/gokz/jumpstats.inc index 3184507d..45e02142 100644 --- a/addons/sourcemod/scripting/include/gokz/jumpstats.inc +++ b/addons/sourcemod/scripting/include/gokz/jumpstats.inc @@ -186,9 +186,9 @@ stock char gC_JSOptionNames[JSOPTION_COUNT][] = "GOKZ JS - Failstats Console", "GOKZ JS - Failstats Chat", "GOKZ JS - Jumpstats Always", - "GOKZ JS - Extended Chat Report", - "GOKZ JS - Minimal Chat Broadcast Tier", - "GOKZ JS - Minimal Sound Broadcast Tier" + "GOKZ JS - Ext Chat Report", + "GOKZ JS - Min Chat Broadcast", + "GOKZ JS - Min Sound Broadcast" }; stock char gC_JSOptionDescriptions[JSOPTION_COUNT][] = diff --git a/addons/sourcemod/scripting/include/gokz/localdb.inc b/addons/sourcemod/scripting/include/gokz/localdb.inc index 6520a96d..472a1205 100644 --- a/addons/sourcemod/scripting/include/gokz/localdb.inc +++ b/addons/sourcemod/scripting/include/gokz/localdb.inc @@ -128,7 +128,7 @@ enum stock char gC_DBOptionNames[DBOPTION_COUNT][] = { - "GOKZ DB - Auto Load Timer Setup" + "GOKZ DB - Auto Load Setup" }; stock char gC_DBOptionDescriptions[DBOPTION_COUNT][] = diff --git a/addons/sourcemod/translations/gokz-localranks.phrases.txt b/addons/sourcemod/translations/gokz-localranks.phrases.txt index ba1e3d73..4f6b0434 100644 --- a/addons/sourcemod/translations/gokz-localranks.phrases.txt +++ b/addons/sourcemod/translations/gokz-localranks.phrases.txt @@ -380,6 +380,10 @@ "en" "{grey}No jumpstats were found!" "ru" "{grey}Статистика прыжков не найдена!" } + "No Replay for Jump" + { + "en" "{red}No replay for jump found!" + } "Jump Record" { "#format" "{1:s},{2:s},{3:s},{4:.4f}" diff --git a/cfg/sourcemod/gokz/gokz.cfg b/cfg/sourcemod/gokz/gokz.cfg index f7b14f41..8b1ecf95 100644 --- a/cfg/sourcemod/gokz/gokz.cfg +++ b/cfg/sourcemod/gokz/gokz.cfg @@ -58,5 +58,8 @@ sv_mincmdrate 128 sv_minupdaterate 128 mp_warmuptime_all_players_connected 0 +// Team picking +mp_force_pick_time 60 + // Restart round to ensure settings (e.g. mp_weapons_allow_map_placed) are applied mp_restartgame 1