/* 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(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(tickData[0]), 0); // origin[0] playbackTickData[bot].Set(i, view_as(tickData[1]), 1); // origin[1] playbackTickData[bot].Set(i, view_as(tickData[2]), 2); // origin[2] playbackTickData[bot].Set(i, view_as(tickData[3]), 3); // angles[0] playbackTickData[bot].Set(i, view_as(tickData[4]), 4); // angles[1] playbackTickData[bot].Set(i, view_as(tickData[5]), 5); // buttons playbackTickData[bot].Set(i, view_as(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(intPlayerSensitivity); // Player MYAW int intPlayerMYaw; file.ReadInt32(intPlayerMYaw); float playerMYaw = view_as(intPlayerMYaw); // Tickrate int tickrateAsInt; file.ReadInt32(tickrateAsInt); float tickrate = view_as(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(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(botJumpDistance[bot])); // Block Distance file.ReadInt32(botJumpBlockDistance[bot]); // Strafe Count int strafeCount; file.ReadInt8(strafeCount); // Sync float sync; file.ReadInt32(view_as(sync)); // Pre float pre; file.ReadInt32(view_as(pre)); // Max float max; file.ReadInt32(view_as(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( { 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( { 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( { 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( { 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(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(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK"); if(currentTickData.flags & RP_MOVETYPE_MASK == view_as(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER"); if(currentTickData.flags & RP_MOVETYPE_MASK == view_as(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP"); if(currentTickData.flags & RP_MOVETYPE_MASK == view_as(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(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( { 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( { 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(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK"); if(currentTickData.flags & RP_MOVETYPE_MASK == view_as(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER"); if(currentTickData.flags & RP_MOVETYPE_MASK == view_as(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP"); if(currentTickData.flags & RP_MOVETYPE_MASK == view_as(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; }