From aef0d1c1268ab7d4bc18996c9c6b4da16a40aadc Mon Sep 17 00:00:00 2001 From: navewindre Date: Mon, 4 Dec 2023 18:06:10 +0100 Subject: bbbbbbbbwaaaaaaaaaaa --- sourcemod/scripting/gokz-replays/playback.sp | 1501 ++++++++++++++++++++++++++ 1 file changed, 1501 insertions(+) create mode 100644 sourcemod/scripting/gokz-replays/playback.sp (limited to 'sourcemod/scripting/gokz-replays/playback.sp') 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(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; +} \ No newline at end of file -- cgit v1.2.3