diff options
| author | navewindre <nw@moneybot.cc> | 2023-12-04 18:06:10 +0100 |
|---|---|---|
| committer | navewindre <nw@moneybot.cc> | 2023-12-04 18:06:10 +0100 |
| commit | aef0d1c1268ab7d4bc18996c9c6b4da16a40aadc (patch) | |
| tree | 43e766b51704f4ab8b383583bdc1871eeeb9c698 /sourcemod/scripting/gokz-replays | |
| parent | 38f1140c11724da05a23a10385061200b907cf6e (diff) | |
bbbbbbbbwaaaaaaaaaaa
Diffstat (limited to 'sourcemod/scripting/gokz-replays')
| -rw-r--r-- | sourcemod/scripting/gokz-replays/api.sp | 78 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/commands.sp | 55 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/controls.sp | 224 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/nav.sp | 97 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/playback.sp | 1501 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/recording.sp | 990 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/replay_cache.sp | 176 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-replays/replay_menu.sp | 139 |
8 files changed, 3260 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-replays/api.sp b/sourcemod/scripting/gokz-replays/api.sp new file mode 100644 index 0000000..3c115e1 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/api.sp @@ -0,0 +1,78 @@ +static GlobalForward H_OnReplaySaved; +static GlobalForward H_OnReplayDiscarded; +static GlobalForward H_OnTimerEnd_Post; + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_RP_GetPlaybackInfo", Native_RP_GetPlaybackInfo); + CreateNative("GOKZ_RP_LoadJumpReplay", Native_RP_LoadJumpReplay); + CreateNative("GOKZ_RP_UpdateReplayControlMenu", Native_RP_UpdateReplayControlMenu); +} + +public int Native_RP_GetPlaybackInfo(Handle plugin, int numParams) +{ + HUDInfo info; + GetPlaybackState(GetNativeCell(1), info); + SetNativeArray(2, info, sizeof(HUDInfo)); + return 1; +} + +public int Native_RP_LoadJumpReplay(Handle plugin, int numParams) +{ + int len; + GetNativeStringLength(2, len); + char[] path = new char[len + 1]; + GetNativeString(2, path, len + 1); + int botClient = LoadReplayBot(GetNativeCell(1), path); + return botClient; +} + +public int Native_RP_UpdateReplayControlMenu(Handle plugin, int numParams) +{ + return view_as<int>(UpdateReplayControlMenu(GetNativeCell(1))); +} + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnReplaySaved = new GlobalForward("GOKZ_RP_OnReplaySaved", ET_Event, Param_Cell, Param_Cell, Param_String, Param_Cell, Param_Cell, Param_Float, Param_String, Param_Cell); + H_OnReplayDiscarded = new GlobalForward("GOKZ_RP_OnReplayDiscarded", ET_Ignore, Param_Cell); + H_OnTimerEnd_Post = new GlobalForward("GOKZ_RP_OnTimerEnd_Post", ET_Ignore, Param_Cell, Param_String, Param_Cell, Param_Float, Param_Cell); +} + +Action Call_OnReplaySaved(int client, int replayType, const char[] map, int course, int timeType, float time, const char[] filePath, bool tempReplay) +{ + Action result; + Call_StartForward(H_OnReplaySaved); + Call_PushCell(client); + Call_PushCell(replayType); + Call_PushString(map); + Call_PushCell(course); + Call_PushCell(timeType); + Call_PushFloat(time); + Call_PushString(filePath); + Call_PushCell(tempReplay); + Call_Finish(result); + return result; +} + +void Call_OnReplayDiscarded(int client) +{ + Call_StartForward(H_OnReplayDiscarded); + Call_PushCell(client); + Call_Finish(); +} + +void Call_OnTimerEnd_Post(int client, const char[] filePath, int course, float time, int teleportsUsed) +{ + Call_StartForward(H_OnTimerEnd_Post); + Call_PushCell(client); + Call_PushString(filePath); + Call_PushCell(course); + Call_PushFloat(time); + Call_PushCell(teleportsUsed); + Call_Finish(); +} diff --git a/sourcemod/scripting/gokz-replays/commands.sp b/sourcemod/scripting/gokz-replays/commands.sp new file mode 100644 index 0000000..43251f6 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/commands.sp @@ -0,0 +1,55 @@ +void RegisterCommands() +{ + RegConsoleCmd("sm_replay", CommandReplay, "[KZ] Open the replay loading menu."); + RegConsoleCmd("sm_replaycontrols", CommandReplayControls, "[KZ] Toggle the replay control menu."); + RegConsoleCmd("sm_rpcontrols", CommandReplayControls, "[KZ] Toggle the replay control menu."); + RegConsoleCmd("sm_replaygoto", CommandReplayGoto, "[KZ] Skip to a specific time in the replay (hh:mm:ss)."); + RegConsoleCmd("sm_rpgoto", CommandReplayGoto, "[KZ] Skip to a specific time in the replay (hh:mm:ss)."); +} + +public Action CommandReplay(int client, int args) +{ + DisplayReplayModeMenu(client); + return Plugin_Handled; +} + +public Action CommandReplayControls(int client, int args) +{ + ToggleReplayControls(client); + return Plugin_Handled; +} + +public Action CommandReplayGoto(int client, int args) +{ + int seconds; + char timeString[32], split[3][32]; + + GetCmdArgString(timeString, sizeof(timeString)); + int res = ExplodeString(timeString, ":", split, 3, 32, false); + switch (res) + { + case 1: + { + seconds = StringToInt(split[0]); + } + + case 2: + { + seconds = StringToInt(split[0]) * 60 + StringToInt(split[1]); + } + + case 3: + { + seconds = StringToInt(split[0]) * 3600 + StringToInt(split[1]) * 60 + StringToInt(split[2]); + } + + default: + { + GOKZ_PrintToChat(client, true, "%t", "Replay Controls - Invalid Time"); + return Plugin_Handled; + } + } + + TrySkipToTime(client, seconds); + return Plugin_Handled; +} diff --git a/sourcemod/scripting/gokz-replays/controls.sp b/sourcemod/scripting/gokz-replays/controls.sp new file mode 100644 index 0000000..cda7f07 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/controls.sp @@ -0,0 +1,224 @@ +/* + Lets player control the replay bot. +*/ + +#define ITEM_INFO_PAUSE "pause" +#define ITEM_INFO_SKIP "skip" +#define ITEM_INFO_REWIND "rewind" +#define ITEM_INFO_FREECAM "freecam" + +static int controllingPlayer[RP_MAX_BOTS]; +static int botTeleports[RP_MAX_BOTS]; +static bool showReplayControls[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +void OnPlayerRunCmdPost_ReplayControls(int client, int cmdnum) +{ + // Let the HUD plugin takes care of this if possible. + if (cmdnum % 6 == 3 && !gB_GOKZHUD) + { + UpdateReplayControlMenu(client); + } +} + +bool UpdateReplayControlMenu(int client) +{ + if (!IsValidClient(client) || IsFakeClient(client)) + { + return false; + } + + int botClient = GetObserverTarget(client); + int bot = GetBotFromClient(botClient); + if (bot == -1) + { + return false; + } + + if (!IsReplayBotControlled(bot, botClient) && !InBreather(bot)) + { + CancelReplayControlsForBot(bot); + controllingPlayer[bot] = client; + } + else if (controllingPlayer[bot] != client) + { + return false; + } + + if (showReplayControls[client] && + GOKZ_HUD_GetOption(client, HUDOption_ShowControls) == ReplayControls_Enabled) + { + // We have to update this often if bot uses teleports. + if (GetClientMenu(client) == MenuSource_None || + GOKZ_HUD_GetMenuShowing(client) && GetClientAvgLoss(client, NetFlow_Both) > EPSILON || + GOKZ_HUD_GetMenuShowing(client) && GOKZ_HUD_GetOption(client, HUDOption_TimerText) == TimerText_TPMenu || + GOKZ_HUD_GetMenuShowing(client) && PlaybackGetTeleports(bot) > 0) + { + botTeleports[bot] = PlaybackGetTeleports(bot); + ShowReplayControlMenu(client, bot); + } + return true; + } + return false; +} + +void ShowReplayControlMenu(int client, int bot) +{ + char text[256]; + + Menu menu = new Menu(MenuHandler_ReplayControls); + menu.OptionFlags = MENUFLAG_NO_SOUND; + menu.Pagination = MENU_NO_PAGINATION; + menu.ExitButton = true; + if (gB_GOKZHUD) + { + if (GOKZ_HUD_GetOption(client, HUDOption_ShowSpectators) != ShowSpecs_Disabled && + GOKZ_HUD_GetOption(client, HUDOption_SpecListPosition) == SpecListPosition_TPMenu) + { + HUDInfo info; + GetPlaybackState(client, info); + GOKZ_HUD_GetMenuSpectatorText(client, info, text, sizeof(text)); + } + if (GOKZ_HUD_GetOption(client, HUDOption_TimerText) == TimerText_TPMenu) + { + Format(text, sizeof(text), "%s\n%T - %s", text, "Replay Controls - Title", client, + GOKZ_FormatTime(GetPlaybackTime(bot), GOKZ_HUD_GetOption(client, HUDOption_TimerStyle) == TimerStyle_Precise)); + } + else + { + Format(text, sizeof(text), "%s%T", text, "Replay Controls - Title", client); + } + } + else + { + Format(text, sizeof(text), "%s%T", text, "Replay Controls - Title", client); + } + + + if (botTeleports[bot] > 0) + { + Format(text, sizeof(text), "%s\n%T", text, "Replay Controls - Teleports", client, botTeleports[bot]); + } + + menu.SetTitle(text); + + if (PlaybackPaused(bot)) + { + FormatEx(text, sizeof(text), "%T", "Replay Controls - Resume", client); + menu.AddItem(ITEM_INFO_PAUSE, text); + } + else + { + FormatEx(text, sizeof(text), "%T", "Replay Controls - Pause", client); + menu.AddItem(ITEM_INFO_PAUSE, text); + } + + FormatEx(text, sizeof(text), "%T", "Replay Controls - Skip", client); + menu.AddItem(ITEM_INFO_SKIP, text); + + FormatEx(text, sizeof(text), "%T\n ", "Replay Controls - Rewind", client); + menu.AddItem(ITEM_INFO_REWIND, text); + + FormatEx(text, sizeof(text), "%T", "Replay Controls - Freecam", client); + menu.AddItem(ITEM_INFO_FREECAM, text); + + menu.Display(client, MENU_TIME_FOREVER); + + if (gB_GOKZHUD) + { + GOKZ_HUD_SetMenuShowing(client, true); + } +} + +void ToggleReplayControls(int client) +{ + if (showReplayControls[client]) + { + CancelReplayControls(client); + } + else + { + showReplayControls[client] = true; + } +} + +void EnableReplayControls(int client) +{ + showReplayControls[client] = true; +} + +bool IsReplayBotControlled(int bot, int botClient) +{ + return IsValidClient(controllingPlayer[bot]) && + (GetObserverTarget(controllingPlayer[bot]) == botClient || + GetEntProp(controllingPlayer[bot], Prop_Send, "m_iObserverMode") == 6); +} + +int MenuHandler_ReplayControls(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_Select: + { + if (!IsValidClient(param1)) + { + return; + } + + int bot = GetBotFromClient(GetObserverTarget(param1)); + if (bot == -1 || controllingPlayer[bot] != param1) + { + return; + } + + char info[16]; + menu.GetItem(param2, info, sizeof(info)); + if (StrEqual(info, ITEM_INFO_PAUSE, false)) + { + PlaybackTogglePause(bot); + } + else if (StrEqual(info, ITEM_INFO_SKIP, false)) + { + PlaybackSkipForward(bot); + } + else if (StrEqual(info, ITEM_INFO_REWIND, false)) + { + PlaybackSkipBack(bot); + } + else if (StrEqual(info, ITEM_INFO_FREECAM, false)) + { + SetEntProp(param1, Prop_Send, "m_iObserverMode", 6); + } + GOKZ_HUD_SetMenuShowing(param1, false); + } + case MenuAction_Cancel: + { + GOKZ_HUD_SetMenuShowing(param1, false); + if (param2 == MenuCancel_Exit) + { + CancelReplayControls(param1); + } + } + case MenuAction_End: + { + delete menu; + } + } +} + +void CancelReplayControls(int client) +{ + if (IsValidClient(client) && showReplayControls[client]) + { + CancelClientMenu(client); + showReplayControls[client] = false; + } +} + +void CancelReplayControlsForBot(int bot) +{ + CancelReplayControls(controllingPlayer[bot]); +} diff --git a/sourcemod/scripting/gokz-replays/nav.sp b/sourcemod/scripting/gokz-replays/nav.sp new file mode 100644 index 0000000..4e73c2f --- /dev/null +++ b/sourcemod/scripting/gokz-replays/nav.sp @@ -0,0 +1,97 @@ +/* + Ensures that there is .nav file for the map so the server + does not to auto-generating one. +*/ + + + +// =====[ EVENTS ]===== + +void OnMapStart_Nav() +{ + if (!CheckForNavFile()) + { + GenerateNavFile(); + } +} + + + +// =====[ PRIVATE ]===== + +static bool CheckForNavFile() +{ + // Make sure there's a nav file + // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + + char mapPath[PLATFORM_MAX_PATH]; + GetCurrentMap(mapPath, sizeof(mapPath)); + + char navFilePath[PLATFORM_MAX_PATH]; + FormatEx(navFilePath, PLATFORM_MAX_PATH, "maps/%s.nav", mapPath); + + return FileExists(navFilePath); +} + +static void GenerateNavFile() +{ + // Generate (copy a) .nav file for the map + // Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + + char mapPath[PLATFORM_MAX_PATH]; + GetCurrentMap(mapPath, sizeof(mapPath)); + + char[] navFilePath = new char[PLATFORM_MAX_PATH]; + FormatEx(navFilePath, PLATFORM_MAX_PATH, "maps/%s.nav", mapPath); + + if (!FileExists(RP_NAV_FILE)) + { + SetFailState("Failed to load file: \"%s\". Check that it exists.", RP_NAV_FILE); + } + File_Copy(RP_NAV_FILE, navFilePath); + ForceChangeLevel(gC_CurrentMap, "[gokz-replays] Generate .nav file."); +} + +/* + * Copies file source to destination + * Based on code of javalia: + * http://forums.alliedmods.net/showthread.php?t=159895 + * + * Credit to shavit's simple bhop timer - https://github.com/shavitush/bhoptimer + * + * @param source Input file + * @param destination Output file + */ +static bool File_Copy(const char[] source, const char[] destination) +{ + File file_source = OpenFile(source, "rb"); + + if (file_source == null) + { + return false; + } + + File file_destination = OpenFile(destination, "wb"); + + if (file_destination == null) + { + delete file_source; + + return false; + } + + int[] buffer = new int[32]; + int cache = 0; + + while (!IsEndOfFile(file_source)) + { + cache = ReadFile(file_source, buffer, 32, 1); + + file_destination.Write(buffer, cache, 1); + } + + delete file_source; + delete file_destination; + + return true; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays/playback.sp b/sourcemod/scripting/gokz-replays/playback.sp new file mode 100644 index 0000000..b3f6865 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/playback.sp @@ -0,0 +1,1501 @@ +/* + Bot replay playback logic and processes. + + The recorded files are read and their information and tick data + stored into variables. A bot is then used to playback the recorded + data by setting it's origin, velocity, etc. in OnPlayerRunCmd. +*/ + + + +static int preAndPostRunTickCount; + +static int playbackTick[RP_MAX_BOTS]; +static ArrayList playbackTickData[RP_MAX_BOTS]; +static bool inBreather[RP_MAX_BOTS]; +static float breatherStartTime[RP_MAX_BOTS]; + +// Original bot caller, needed for OnClientPutInServer callback +static int botCaller[RP_MAX_BOTS]; +// Original bot name after creation by bot_add, needed for bot removal +static char botName[RP_MAX_BOTS][MAX_NAME_LENGTH]; +static bool botInGame[RP_MAX_BOTS]; +static int botClient[RP_MAX_BOTS]; +static bool botDataLoaded[RP_MAX_BOTS]; +static int botReplayType[RP_MAX_BOTS]; +static int botReplayVersion[RP_MAX_BOTS]; +static int botSteamAccountID[RP_MAX_BOTS]; +static int botCourse[RP_MAX_BOTS]; +static int botMode[RP_MAX_BOTS]; +static int botStyle[RP_MAX_BOTS]; +static float botTime[RP_MAX_BOTS]; +static int botTimeTicks[RP_MAX_BOTS]; +static char botAlias[RP_MAX_BOTS][MAX_NAME_LENGTH]; +static bool botPaused[RP_MAX_BOTS]; +static bool botPlaybackPaused[RP_MAX_BOTS]; +static int botKnife[RP_MAX_BOTS]; +static int botWeapon[RP_MAX_BOTS]; +static int botJumpType[RP_MAX_BOTS]; +static float botJumpDistance[RP_MAX_BOTS]; +static int botJumpBlockDistance[RP_MAX_BOTS]; + +static int timeOnGround[RP_MAX_BOTS]; +static int timeInAir[RP_MAX_BOTS]; +static int botTeleportsUsed[RP_MAX_BOTS]; +static int botCurrentTeleport[RP_MAX_BOTS]; +static int botButtons[RP_MAX_BOTS]; +static MoveType botMoveType[RP_MAX_BOTS]; +static float botTakeoffSpeed[RP_MAX_BOTS]; +static float botSpeed[RP_MAX_BOTS]; +static float botLastOrigin[RP_MAX_BOTS][3]; +static bool hitBhop[RP_MAX_BOTS]; +static bool hitPerf[RP_MAX_BOTS]; +static bool botJumped[RP_MAX_BOTS]; +static bool botIsTakeoff[RP_MAX_BOTS]; +static bool botJustTeleported[RP_MAX_BOTS]; +static float botLandingSpeed[RP_MAX_BOTS]; + + + +// =====[ PUBLIC ]===== + +// Returns the client index of the replay bot, or -1 otherwise +int LoadReplayBot(int client, char[] path) +{ + // Safeguard Check + if (GOKZ_GetCoreOption(client, Option_Safeguard) > Safeguard_Disabled && GOKZ_GetTimerRunning(client) && GOKZ_GetValidTimer(client)) + { + if (!GOKZ_GetPaused(client) && !GOKZ_GetCanPause(client)) + { + GOKZ_PrintToChat(client, true, "%t", "Safeguard - Blocked"); + GOKZ_PlayErrorSound(client); + return -1; + } + } + int bot; + if (GetBotsInUse() < RP_MAX_BOTS) + { + bot = GetUnusedBot(); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Bots Available"); + GOKZ_PlayErrorSound(client); + return -1; + } + + if (bot == -1) + { + LogError("Unused bot could not be found even though only %d out of %d are known to be in use.", + GetBotsInUse(), RP_MAX_BOTS); + GOKZ_PlayErrorSound(client); + return -1; + } + + if (!LoadPlayback(client, bot, path)) + { + GOKZ_PlayErrorSound(client); + return -1; + } + + ServerCommand("bot_add"); + botCaller[bot] = client; + return botClient[bot]; +} + +// Passes the current state of the replay into the HUDInfo struct +void GetPlaybackState(int client, HUDInfo info) +{ + int bot, i; + for(i = 0; i < RP_MAX_BOTS; i++) + { + bot = botClient[i] == client ? i : bot; + } + if (i == RP_MAX_BOTS + 1) return; + + if (playbackTickData[bot] == INVALID_HANDLE) + { + return; + } + + info.TimerRunning = botReplayType[bot] == ReplayType_Jump ? false : true; + if (botReplayVersion[bot] == 1) + { + info.Time = playbackTick[bot] * GetTickInterval(); + } + else if (botReplayVersion[bot] == 2) + { + if (playbackTick[bot] < preAndPostRunTickCount) + { + info.Time = 0.0; + } + else if (playbackTick[bot] >= playbackTickData[bot].Length - preAndPostRunTickCount) + { + info.Time = botTime[bot]; + } + else if (playbackTick[bot] >= preAndPostRunTickCount) + { + info.Time = (playbackTick[bot] - preAndPostRunTickCount) * GetTickInterval(); + } + } + info.TimerRunning = true; + info.TimeType = botTeleportsUsed[bot] > 0 ? TimeType_Nub : TimeType_Pro; + info.Speed = botSpeed[bot]; + info.Paused = false; + info.OnLadder = (botMoveType[bot] == MOVETYPE_LADDER); + info.Noclipping = false; + info.OnGround = Movement_GetOnGround(client); + info.Ducking = botButtons[bot] & IN_DUCK > 0; + info.ID = botClient[bot]; + info.Jumped = botJumped[bot]; + info.HitBhop = hitBhop[bot]; + info.HitPerf = hitPerf[bot]; + info.Buttons = botButtons[bot]; + info.TakeoffSpeed = botTakeoffSpeed[bot]; + info.IsTakeoff = botIsTakeoff[bot] && !Movement_GetOnGround(client); + info.CurrentTeleport = botCurrentTeleport[bot]; +} + +int GetBotFromClient(int client) +{ + for (int bot = 0; bot < RP_MAX_BOTS; bot++) + { + if (botClient[bot] == client) + { + return bot; + } + } + return -1; +} + +bool InBreather(int bot) +{ + return inBreather[bot]; +} + +bool PlaybackPaused(int bot) +{ + return botPlaybackPaused[bot]; +} + +void PlaybackTogglePause(int bot) +{ + if(botPlaybackPaused[bot]) + { + botPlaybackPaused[bot] = false; + } + else + { + botPlaybackPaused[bot] = true; + } +} + +void PlaybackSkipForward(int bot) +{ + if (playbackTick[bot] + RoundToZero(RP_SKIP_TIME / GetTickInterval()) < playbackTickData[bot].Length) + { + PlaybackSkipToTick(bot, playbackTick[bot] + RoundToZero(RP_SKIP_TIME / GetTickInterval())); + } +} + +void PlaybackSkipBack(int bot) +{ + if (playbackTick[bot] < RoundToZero(RP_SKIP_TIME / GetTickInterval())) + { + PlaybackSkipToTick(bot, 0); + } + else + { + PlaybackSkipToTick(bot, playbackTick[bot] - RoundToZero(RP_SKIP_TIME / GetTickInterval())); + } +} + +int PlaybackGetTeleports(int bot) +{ + return botCurrentTeleport[bot]; +} + +void TrySkipToTime(int client, int seconds) +{ + if (!IsValidClient(client)) + { + return; + } + + int tick = seconds * 128 + preAndPostRunTickCount; + int bot = GetBotFromClient(GetObserverTarget(client)); + + if (tick >= 0 && tick < playbackTickData[bot].Length) + { + PlaybackSkipToTick(bot, tick); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Replay Controls - Invalid Time"); + } +} + +float GetPlaybackTime(int bot) +{ + if (playbackTick[bot] < preAndPostRunTickCount) + { + return 0.0; + } + if (playbackTick[bot] >= playbackTickData[bot].Length - preAndPostRunTickCount) + { + return botTime[bot]; + } + if (playbackTick[bot] >= preAndPostRunTickCount) + { + return (playbackTick[bot] - preAndPostRunTickCount) * GetTickInterval(); + } + + return 0.0; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_Playback(int client) +{ + if (!IsFakeClient(client) || IsClientSourceTV(client)) + { + return; + } + + // Check if an unassigned bot has joined, and assign it + for (int bot; bot < RP_MAX_BOTS; bot++) + { + // Also check if the bot was created by us. + if (!botInGame[bot] && botCaller[bot] != 0) + { + botInGame[bot] = true; + botClient[bot] = client; + GetClientName(client, botName[bot], sizeof(botName[])); + // The bot won't receive its weapons properly if we don't wait a frame + RequestFrame(SetBotStuff, bot); + if (IsValidClient(botCaller[bot])) + { + MakePlayerSpectate(botCaller[bot], botClient[bot]); + botCaller[bot] = 0; + } + break; + } + } +} + +void OnClientDisconnect_Playback(int client) +{ + for (int bot; bot < RP_MAX_BOTS; bot++) + { + if (botClient[bot] != client) + { + continue; + } + + botInGame[bot] = false; + if (playbackTickData[bot] != null) + { + playbackTickData[bot].Clear(); // Clear it all out + botDataLoaded[bot] = false; + } + } +} + +void OnPlayerRunCmd_Playback(int client, int &buttons, float vel[3], float angles[3]) +{ + if (!IsFakeClient(client)) + { + return; + } + + for (int bot; bot < RP_MAX_BOTS; bot++) + { + // Check if not the bot we're looking for + if (!botInGame[bot] || botClient[bot] != client || !botDataLoaded[bot]) + { + continue; + } + + switch (botReplayVersion[bot]) + { + case 1: PlaybackVersion1(client, bot, buttons); + case 2: PlaybackVersion2(client, bot, buttons, vel, angles); + } + break; + } +} + +void OnPlayerRunCmdPost_Playback(int client) +{ + for (int bot; bot < RP_MAX_BOTS; bot++) + { + // Check if not the bot we're looking for + if (!botInGame[bot] || botClient[bot] != client || !botDataLoaded[bot]) + { + continue; + } + if (botReplayVersion[bot] == 2) + { + PlaybackVersion2Post(client, bot); + } + break; + } +} + +void GOKZ_OnOptionsLoaded_Playback(int client) +{ + for (int bot = 0; bot < RP_MAX_BOTS; bot++) + { + if (botClient[bot] == client) + { + // Reset its movement options as it might be wrongfully changed + GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]); + GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]); + } + } +} +// =====[ PRIVATE ]===== + +// Returns false if there was a problem loading the playback e.g. doesn't exist +static bool LoadPlayback(int client, int bot, char[] path) +{ + if (!FileExists(path)) + { + GOKZ_PrintToChat(client, true, "%t", "No Replay Found"); + return false; + } + + File file = OpenFile(path, "rb"); + + // Check magic number in header + int magicNumber; + file.ReadInt32(magicNumber); + if (magicNumber != RP_MAGIC_NUMBER) + { + LogError("Failed to load invalid replay file: \"%s\".", path); + delete file; + return false; + } + + // Check replay format version + int formatVersion; + file.ReadInt8(formatVersion); + switch(formatVersion) + { + case 1: + { + botReplayVersion[bot] = 1; + if (!LoadFormatVersion1Replay(file, bot)) + { + return false; + } + } + case 2: + { + botReplayVersion[bot] = 2; + if (!LoadFormatVersion2Replay(file, client, bot)) + { + return false; + } + } + + default: + { + LogError("Failed to load replay file with unsupported format version: \"%s\".", path); + delete file; + return false; + } + } + + return true; +} + +static bool LoadFormatVersion1Replay(File file, int bot) +{ + // Old replays only support runs, not jumps + botReplayType[bot] = ReplayType_Run; + + int length; + + // GOKZ version + file.ReadInt8(length); + char[] gokzVersion = new char[length + 1]; + file.ReadString(gokzVersion, length, length); + gokzVersion[length] = '\0'; + + // Map name + file.ReadInt8(length); + char[] mapName = new char[length + 1]; + file.ReadString(mapName, length, length); + mapName[length] = '\0'; + + // Some integers... + file.ReadInt32(botCourse[bot]); + file.ReadInt32(botMode[bot]); + file.ReadInt32(botStyle[bot]); + + // Old replays don't store the weapon information + botKnife[bot] = CS_WeaponIDToItemDefIndex(CSWeapon_KNIFE); + botWeapon[bot] = (botMode[bot] == Mode_Vanilla) ? -1 : CS_WeaponIDToItemDefIndex(CSWeapon_USP_SILENCER); + + // Time + int timeAsInt; + file.ReadInt32(timeAsInt); + botTime[bot] = view_as<float>(timeAsInt); + + // Some integers... + file.ReadInt32(botTeleportsUsed[bot]); + file.ReadInt32(botSteamAccountID[bot]); + + // SteamID2 + file.ReadInt8(length); + char[] steamID2 = new char[length + 1]; + file.ReadString(steamID2, length, length); + steamID2[length] = '\0'; + + // IP + file.ReadInt8(length); + char[] IP = new char[length + 1]; + file.ReadString(IP, length, length); + IP[length] = '\0'; + + // Alias + file.ReadInt8(length); + file.ReadString(botAlias[bot], sizeof(botAlias[]), length); + botAlias[bot][length] = '\0'; + + // Read tick data + file.ReadInt32(length); + + // Setup playback tick data array list + if (playbackTickData[bot] == null) + { + playbackTickData[bot] = new ArrayList(IntMax(RP_V1_TICK_DATA_BLOCKSIZE, sizeof(ReplayTickData)), length); + } + else + { // Make sure it's all clear and the correct size + playbackTickData[bot].Clear(); + playbackTickData[bot].Resize(length); + } + + // The replay has no replay data, this shouldn't happen normally, + // but this would cause issues in other code, so we don't even try to load this. + if (length == 0) + { + delete file; + return false; + } + + any tickData[RP_V1_TICK_DATA_BLOCKSIZE]; + for (int i = 0; i < length; i++) + { + file.Read(tickData, RP_V1_TICK_DATA_BLOCKSIZE, 4); + playbackTickData[bot].Set(i, view_as<float>(tickData[0]), 0); // origin[0] + playbackTickData[bot].Set(i, view_as<float>(tickData[1]), 1); // origin[1] + playbackTickData[bot].Set(i, view_as<float>(tickData[2]), 2); // origin[2] + playbackTickData[bot].Set(i, view_as<float>(tickData[3]), 3); // angles[0] + playbackTickData[bot].Set(i, view_as<float>(tickData[4]), 4); // angles[1] + playbackTickData[bot].Set(i, view_as<int>(tickData[5]), 5); // buttons + playbackTickData[bot].Set(i, view_as<int>(tickData[6]), 6); // flags + } + + playbackTick[bot] = 0; + botDataLoaded[bot] = true; + + delete file; + return true; +} + +static bool LoadFormatVersion2Replay(File file, int client, int bot) +{ + int length; + + // Replay type + int replayType; + file.ReadInt8(replayType); + + // GOKZ version + file.ReadInt8(length); + char[] gokzVersion = new char[length + 1]; + file.ReadString(gokzVersion, length, length); + gokzVersion[length] = '\0'; + + // Map name + file.ReadInt8(length); + char[] mapName = new char[length + 1]; + file.ReadString(mapName, length, length); + mapName[length] = '\0'; + if (!StrEqual(mapName, gC_CurrentMap)) + { + GOKZ_PrintToChat(client, true, "%t", "Replay Menu - Wrong Map", mapName); + delete file; + return false; + } + + // Map filesize + int mapFileSize; + file.ReadInt32(mapFileSize); + + // Server IP + int serverIP; + file.ReadInt32(serverIP); + + // Timestamp + int timestamp; + file.ReadInt32(timestamp); + + // Player Alias + file.ReadInt8(length); + file.ReadString(botAlias[bot], sizeof(botAlias[]), length); + botAlias[bot][length] = '\0'; + + // Player Steam ID + int steamID; + file.ReadInt32(steamID); + + // Mode + file.ReadInt8(botMode[bot]); + + // Style + file.ReadInt8(botStyle[bot]); + + // Player Sensitivity + int intPlayerSensitivity; + file.ReadInt32(intPlayerSensitivity); + float playerSensitivity = view_as<float>(intPlayerSensitivity); + + // Player MYAW + int intPlayerMYaw; + file.ReadInt32(intPlayerMYaw); + float playerMYaw = view_as<float>(intPlayerMYaw); + + // Tickrate + int tickrateAsInt; + file.ReadInt32(tickrateAsInt); + float tickrate = view_as<float>(tickrateAsInt); + if (tickrate != RoundToZero(1 / GetTickInterval())) + { + GOKZ_PrintToChat(client, true, "%t", "Replay Menu - Wrong Tickrate", tickrate, (RoundToZero(1 / GetTickInterval()))); + delete file; + return false; + } + + // Tick Count + int tickCount; + file.ReadInt32(tickCount); + + // The replay has no replay data, this shouldn't happen normally, + // but this would cause issues in other code, so we don't even try to load this. + if (tickCount == 0) + { + delete file; + return false; + } + + // Equipped Weapon + file.ReadInt32(botWeapon[bot]); + + // Equipped Knife + file.ReadInt32(botKnife[bot]); + + // Big spit to console + PrintToConsole(client, "Replay Type: %d\nGOKZ Version: %s\nMap Name: %s\nMap Filesize: %d\nServer IP: %d\nTimestamp: %d\nPlayer Alias: %s\nPlayer Steam ID: %d\nMode: %d\nStyle: %d\nPlayer Sensitivity: %f\nPlayer m_yaw: %f\nTickrate: %f\nTick Count: %d\nWeapon: %d\nKnife: %d", replayType, gokzVersion, mapName, mapFileSize, serverIP, timestamp, botAlias[bot], steamID, botMode[bot], botStyle[bot], playerSensitivity, playerMYaw, tickrate, tickCount, botWeapon[bot], botKnife[bot]); + + switch(replayType) + { + case ReplayType_Run: + { + // Time + int timeAsInt; + file.ReadInt32(timeAsInt); + botTime[bot] = view_as<float>(timeAsInt); + botTimeTicks[bot] = RoundToNearest(botTime[bot] * tickrate); + + // Course + file.ReadInt8(botCourse[bot]); + + // Teleports Used + file.ReadInt32(botTeleportsUsed[bot]); + + // Type + botReplayType[bot] = ReplayType_Run; + + // Finish spit to console + PrintToConsole(client, "Time: %f\nCourse: %d\nTeleports Used: %d", botTime[bot], botCourse[bot], botTeleportsUsed[bot]); + } + case ReplayType_Cheater: + { + // Reason + int reason; + file.ReadInt8(reason); + + // Type + botReplayType[bot] = ReplayType_Cheater; + + // Finish spit to console + PrintToConsole(client, "AC Reason: %s", gC_ACReasons[reason]); + } + case ReplayType_Jump: + { + // Jump Type + file.ReadInt8(botJumpType[bot]); + + // Distance + file.ReadInt32(view_as<int>(botJumpDistance[bot])); + + // Block Distance + file.ReadInt32(botJumpBlockDistance[bot]); + + // Strafe Count + int strafeCount; + file.ReadInt8(strafeCount); + + // Sync + float sync; + file.ReadInt32(view_as<int>(sync)); + + // Pre + float pre; + file.ReadInt32(view_as<int>(pre)); + + // Max + float max; + file.ReadInt32(view_as<int>(max)); + + // Airtime + int airtime; + file.ReadInt32(airtime); + + // Type + botReplayType[bot] = ReplayType_Jump; + + // Finish spit to console + PrintToConsole(client, "Jump Type: %s\nJump Distance: %f\nBlock Distance: %d\nStrafe Count: %d\nSync: %f\n Pre: %f\nMax: %f\nAirtime: %d", + gC_JumpTypes[botJumpType[bot]], botJumpDistance[bot], botJumpBlockDistance[bot], strafeCount, sync, pre, max, airtime); + } + } + + // Tick Data + // Setup playback tick data array list + if (playbackTickData[bot] == null) + { + playbackTickData[bot] = new ArrayList(IntMax(RP_V1_TICK_DATA_BLOCKSIZE, sizeof(ReplayTickData))); + } + else + { + playbackTickData[bot].Clear(); + } + + // Read tick data + preAndPostRunTickCount = RoundToZero(RP_PLAYBACK_BREATHER_TIME / GetTickInterval()); + any tickDataArray[RP_V2_TICK_DATA_BLOCKSIZE]; + for (int i = 0; i < tickCount; i++) + { + file.ReadInt32(tickDataArray[RPDELTA_DELTAFLAGS]); + + for (int index = 1; index < sizeof(tickDataArray); index++) + { + int currentFlag = (1 << index); + if (tickDataArray[RPDELTA_DELTAFLAGS] & currentFlag) + { + file.ReadInt32(tickDataArray[index]); + } + } + + ReplayTickData tickData; + TickDataFromArray(tickDataArray, tickData); + // HACK: Jump replays don't record proper length sometimes. I don't know why. + // This leads to oversized replays full of 0s at the end. + // So, we do this horrible check to dodge that issue. + if (tickData.origin[0] == 0 && tickData.origin[1] == 0 && tickData.origin[2] == 0 && tickData.angles[0] == 0 && tickData.angles[1] == 0) + { + break; + } + playbackTickData[bot].PushArray(tickData); + } + + playbackTick[bot] = 0; + botDataLoaded[bot] = true; + + delete file; + + return true; +} + +static void PlaybackVersion1(int client, int bot, int &buttons) +{ + int size = playbackTickData[bot].Length; + float repOrigin[3], repAngles[3]; + int repButtons, repFlags; + + // If first or last frame of the playback + if (playbackTick[bot] == 0 || playbackTick[bot] == (size - 1)) + { + // Move the bot and pause them at that tick + repOrigin[0] = playbackTickData[bot].Get(playbackTick[bot], 0); + repOrigin[1] = playbackTickData[bot].Get(playbackTick[bot], 1); + repOrigin[2] = playbackTickData[bot].Get(playbackTick[bot], 2); + repAngles[0] = playbackTickData[bot].Get(playbackTick[bot], 3); + repAngles[1] = playbackTickData[bot].Get(playbackTick[bot], 4); + TeleportEntity(client, repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } )); + + if (!inBreather[bot]) + { + // Start the breather period + inBreather[bot] = true; + breatherStartTime[bot] = GetEngineTime(); + if (playbackTick[bot] == (size - 1)) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End"); + } + } + else if (GetEngineTime() > breatherStartTime[bot] + RP_PLAYBACK_BREATHER_TIME) + { + // End the breather period + inBreather[bot] = false; + botPlaybackPaused[bot] = false; + if (playbackTick[bot] == 0) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start"); + } + // Start the bot if first tick. Clear bot if last tick. + playbackTick[bot]++; + if (playbackTick[bot] == size) + { + playbackTickData[bot].Clear(); // Clear it all out + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + } + } + } + else + { + // Check whether somebody is actually spectating the bot + int spec; + for (spec = 1; spec < MAXPLAYERS + 1; spec++) + { + if (IsValidClient(spec) && GetObserverTarget(spec) == botClient[bot]) + { + break; + } + } + if (spec == MAXPLAYERS + 1 && !IsReplayBotControlled(bot, botClient[bot])) + { + playbackTickData[bot].Clear(); + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + return; + } + + // Load in the next tick + repOrigin[0] = playbackTickData[bot].Get(playbackTick[bot], 0); + repOrigin[1] = playbackTickData[bot].Get(playbackTick[bot], 1); + repOrigin[2] = playbackTickData[bot].Get(playbackTick[bot], 2); + repAngles[0] = playbackTickData[bot].Get(playbackTick[bot], 3); + repAngles[1] = playbackTickData[bot].Get(playbackTick[bot], 4); + repButtons = playbackTickData[bot].Get(playbackTick[bot], 5); + repFlags = playbackTickData[bot].Get(playbackTick[bot], 6); + + // Check if the replay is paused + if (botPlaybackPaused[bot]) + { + TeleportEntity(client, repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } )); + return; + } + + // Set velocity to travel from current origin to recorded origin + float currentOrigin[3], velocity[3]; + Movement_GetOrigin(client, currentOrigin); + MakeVectorFromPoints(currentOrigin, repOrigin, velocity); + ScaleVector(velocity, 128.0); // Hard-coded 128 tickrate + TeleportEntity(client, NULL_VECTOR, repAngles, velocity); + + // We need the velocity directly from the replay to calculate the speeds + // for the HUD. + MakeVectorFromPoints(botLastOrigin[bot], repOrigin, velocity); + ScaleVector(velocity, 128.0); // Hard-coded 128 tickrate + CopyVector(repOrigin, botLastOrigin[bot]); + + botSpeed[bot] = GetVectorHorizontalLength(velocity); + buttons = repButtons; + botButtons[bot] = repButtons; + + // Should the bot be ducking?! + if (repButtons & IN_DUCK || repFlags & FL_DUCKING) + { + buttons |= IN_DUCK; + } + + // If the replay file says the bot's on the ground, then fine! Unless you're going too fast... + // Note that we don't mind if replay file says bot isn't on ground but the bot is. + if (repFlags & FL_ONGROUND && Movement_GetSpeed(client) < SPEED_NORMAL * 2) + { + if (timeInAir[bot] > 0) + { + botLandingSpeed[bot] = botSpeed[bot]; + timeInAir[bot] = 0; + botIsTakeoff[bot] = false; + botJumped[bot] = false; + hitBhop[bot] = false; + hitPerf[bot] = false; + if (!Movement_GetOnGround(client)) + { + timeOnGround[bot] = 0; + } + } + + SetEntityFlags(client, GetEntityFlags(client) | FL_ONGROUND); + Movement_SetMovetype(client, MOVETYPE_WALK); + + timeOnGround[bot]++; + botTakeoffSpeed[bot] = botSpeed[bot]; + } + else + { + if (timeInAir[bot] == 0) + { + botIsTakeoff[bot] = true; + botJumped[bot] = botButtons[bot] & IN_JUMP > 0; + hitBhop[bot] = (timeOnGround[bot] <= RP_MAX_BHOP_GROUND_TICKS) && botJumped[bot]; + + if (botMode[bot] == Mode_SimpleKZ) + { + hitPerf[bot] = timeOnGround[bot] < 3 && botJumped[bot]; + } + else + { + hitPerf[bot] = timeOnGround[bot] < 2 && botJumped[bot]; + } + + if (hitPerf[bot]) + { + if (botMode[bot] == Mode_SimpleKZ) + { + botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], (0.2 * botLandingSpeed[bot] + 200)); + } + else if (botMode[bot] == Mode_KZTimer) + { + botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], 380.0); + } + else + { + botTakeoffSpeed[bot] = FloatMin(botLandingSpeed[bot], 286.0); + } + } + } + else + { + botJumped[bot] = false; + botIsTakeoff[bot] = false; + } + + timeInAir[bot]++; + Movement_SetMovetype(client, MOVETYPE_NOCLIP); + } + + playbackTick[bot]++; + } +} +void PlaybackVersion2(int client, int bot, int &buttons, float vel[3], float angles[3]) +{ + int size = playbackTickData[bot].Length; + ReplayTickData prevTickData; + ReplayTickData currentTickData; + + // If first or last frame of the playback + if (playbackTick[bot] == 0 || playbackTick[bot] == (size - 1)) + { + // Move the bot and pause them at that tick + playbackTickData[bot].GetArray(playbackTick[bot], currentTickData); + playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData); + TeleportEntity(client, currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } )); + + if (!inBreather[bot]) + { + // Start the breather period + inBreather[bot] = true; + breatherStartTime[bot] = GetEngineTime(); + } + else if (GetEngineTime() > breatherStartTime[bot] + RP_PLAYBACK_BREATHER_TIME) + { + // End the breather period + inBreather[bot] = false; + botPlaybackPaused[bot] = false; + + // Start the bot if first tick. Clear bot if last tick. + playbackTick[bot]++; + if (playbackTick[bot] == size) + { + playbackTickData[bot].Clear(); // Clear it all out + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + } + } + } + else + { + // Check whether somebody is actually spectating the bot + int spec; + for (spec = 1; spec < MAXPLAYERS + 1; spec++) + { + if (IsValidClient(spec) && GetObserverTarget(spec) == botClient[bot]) + { + break; + } + } + if (spec == MAXPLAYERS + 1 && !IsReplayBotControlled(bot, botClient[bot])) + { + playbackTickData[bot].Clear(); + botDataLoaded[bot] = false; + CancelReplayControlsForBot(bot); + ServerCommand("bot_kick %s", botName[bot]); + return; + } + + // Load in the next tick + playbackTickData[bot].GetArray(playbackTick[bot], currentTickData); + playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData); + + // Check if the replay is paused + if (botPlaybackPaused[bot]) + { + TeleportEntity(client, currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } )); + return; + } + + // Play timer start/end sound, if necessary. Reset teleports + if (playbackTick[bot] == preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start"); + botCurrentTeleport[bot] = 0; + } + if (playbackTick[bot] == botTimeTicks[bot] + preAndPostRunTickCount && botReplayType[bot] == ReplayType_Run) + { + GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End"); + } + // We use the previous position/velocity data to recreate sounds accurately. + // This might not be necessary as we already did do this in OnPlayerRunCmdPost of last tick, + // but we do it again just in case the values don't match up somehow (eg. collision with moving objects?) + TeleportEntity(client, NULL_VECTOR, prevTickData.angles, prevTickData.velocity); + // TeleportEntity does not set the absolute origin and velocity so we need to do it + // to prevent inaccurate eye position interpolation. + SetEntPropVector(client, Prop_Data, "m_vecVelocity", prevTickData.velocity); + SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", prevTickData.velocity); + + SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", prevTickData.origin); + SetEntPropVector(client, Prop_Data, "m_vecOrigin", prevTickData.origin); + + + // Set buttons and potential inputs. + int newButtons; + if (currentTickData.flags & RP_IN_ATTACK) + { + newButtons |= IN_ATTACK; + } + if (currentTickData.flags & RP_IN_ATTACK2) + { + newButtons |= IN_ATTACK2; + } + if (currentTickData.flags & RP_IN_JUMP) + { + newButtons |= IN_JUMP; + } + if (currentTickData.flags & RP_IN_DUCK || currentTickData.flags & RP_FL_DUCKING) + { + newButtons |= IN_DUCK; + } + // Few assumptions here because the replay doesn't track them: Player doesn't use +klook or +strafe. + // If the assumptions are wrong we will just end up with wrong sound prediction, no big deal. + if (currentTickData.flags & RP_IN_FORWARD) + { + newButtons |= IN_FORWARD; + vel[0] += RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_BACK) + { + newButtons |= IN_BACK; + vel[0] -= RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_MOVELEFT) + { + newButtons |= IN_MOVELEFT; + vel[1] -= RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_MOVERIGHT) + { + newButtons |= IN_MOVERIGHT; + vel[1] += RP_PLAYER_ACCELSPEED; + } + if (currentTickData.flags & RP_IN_LEFT) + { + newButtons |= IN_LEFT; + } + if (currentTickData.flags & RP_IN_RIGHT) + { + newButtons |= IN_RIGHT; + } + if (currentTickData.flags & RP_IN_RELOAD) + { + newButtons |= IN_RELOAD; + } + if (currentTickData.flags & RP_IN_SPEED) + { + newButtons |= IN_SPEED; + } + buttons = newButtons; + botButtons[bot] = buttons; + // The angles might be wrong if the player teleports, but this should only affect sound prediction. + angles = currentTickData.angles; + + // Set the bot's MoveType + MoveType replayMoveType = view_as<MoveType>(prevTickData.flags & RP_MOVETYPE_MASK); + botMoveType[bot] = replayMoveType; + if (replayMoveType == MOVETYPE_WALK) + { + Movement_SetMovetype(client, MOVETYPE_WALK); + } + else if (replayMoveType == MOVETYPE_LADDER) + { + botPaused[bot] = false; + Movement_SetMovetype(client, MOVETYPE_LADDER); + } + else + { + Movement_SetMovetype(client, MOVETYPE_NOCLIP); + } + // Set some variables + if (currentTickData.flags & RP_TELEPORT_TICK) + { + botJustTeleported[bot] = true; + botCurrentTeleport[bot]++; + } + + if (currentTickData.flags & RP_TAKEOFF_TICK) + { + hitPerf[bot] = currentTickData.flags & RP_HIT_PERF > 0; + botIsTakeoff[bot] = true; + botTakeoffSpeed[bot] = GetVectorHorizontalLength(currentTickData.velocity); + } + + if ((currentTickData.flags & RP_SECONDARY_EQUIPPED) && !IsCurrentWeaponSecondary(client)) + { + int item = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + if (item != -1) + { + char name[64]; + GetEntityClassname(item, name, sizeof(name)); + FakeClientCommand(client, "use %s", name); + } + } + else if (!(currentTickData.flags & RP_SECONDARY_EQUIPPED) && IsCurrentWeaponSecondary(client)) + { + int item = GetPlayerWeaponSlot(client, CS_SLOT_KNIFE); + if (item != -1) + { + char name[64]; + GetEntityClassname(item, name, sizeof(name)); + FakeClientCommand(client, "use %s", name); + } + } + + #if defined DEBUG + if(!botPlaybackPaused[bot]) + { + PrintToServer("Tick: %d", playbackTick[bot]); + PrintToServer("X %f \nY %f \nZ %f\nPitch %f\nYaw %f", currentTickData.origin[0], currentTickData.origin[1], currentTickData.origin[2], currentTickData.angles[0], currentTickData.angles[1]); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NONE"); + + if(currentTickData.flags & RP_IN_ATTACK) PrintToServer("IN_ATTACK"); + if(currentTickData.flags & RP_IN_ATTACK2) PrintToServer("IN_ATTACK2"); + if(currentTickData.flags & RP_IN_JUMP) PrintToServer("IN_JUMP"); + if(currentTickData.flags & RP_IN_DUCK) PrintToServer("IN_DUCK"); + if(currentTickData.flags & RP_IN_FORWARD) PrintToServer("IN_FORWARD"); + if(currentTickData.flags & RP_IN_BACK) PrintToServer("IN_BACK"); + if(currentTickData.flags & RP_IN_LEFT) PrintToServer("IN_LEFT"); + if(currentTickData.flags & RP_IN_RIGHT) PrintToServer("IN_RIGHT"); + if(currentTickData.flags & RP_IN_MOVELEFT) PrintToServer("IN_MOVELEFT"); + if(currentTickData.flags & RP_IN_MOVERIGHT) PrintToServer("IN_MOVERIGHT"); + if(currentTickData.flags & RP_IN_RELOAD) PrintToServer("IN_RELOAD"); + if(currentTickData.flags & RP_IN_SPEED) PrintToServer("IN_SPEED"); + if(currentTickData.flags & RP_IN_USE) PrintToServer("IN_USE"); + if(currentTickData.flags & RP_IN_BULLRUSH) PrintToServer("IN_BULLRUSH"); + + if(currentTickData.flags & RP_FL_ONGROUND) PrintToServer("FL_ONGROUND"); + if(currentTickData.flags & RP_FL_DUCKING ) PrintToServer("FL_DUCKING"); + if(currentTickData.flags & RP_FL_SWIM) PrintToServer("FL_SWIM"); + if(currentTickData.flags & RP_UNDER_WATER) PrintToServer("WATERLEVEL!=0"); + if(currentTickData.flags & RP_TELEPORT_TICK) PrintToServer("TELEPORT"); + if(currentTickData.flags & RP_TAKEOFF_TICK) PrintToServer("TAKEOFF"); + if(currentTickData.flags & RP_HIT_PERF) PrintToServer("PERF"); + if(currentTickData.flags & RP_SECONDARY_EQUIPPED) PrintToServer("SECONDARY_WEAPON_EQUIPPED"); + PrintToServer("=============================================================="); + } + #endif + } +} + +void PlaybackVersion2Post(int client, int bot) +{ + if (botPlaybackPaused[bot]) + { + return; + } + int size = playbackTickData[bot].Length; + if (playbackTick[bot] != 0 && playbackTick[bot] != (size - 1)) + { + ReplayTickData currentTickData; + ReplayTickData prevTickData; + playbackTickData[bot].GetArray(playbackTick[bot], currentTickData); + playbackTickData[bot].GetArray(IntMax(playbackTick[bot] - 1, 0), prevTickData); + + // TeleportEntity does not set the absolute origin and velocity so we need to do it + // to prevent inaccurate eye position interpolation. + SetEntPropVector(client, Prop_Data, "m_vecVelocity", currentTickData.velocity); + SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", currentTickData.velocity); + + SetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", currentTickData.origin); + SetEntPropVector(client, Prop_Data, "m_vecOrigin", currentTickData.origin); + + SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[0]", currentTickData.angles[0]); + SetEntPropFloat(client, Prop_Send, "m_angEyeAngles[1]", currentTickData.angles[1]); + + MoveType replayMoveType = view_as<MoveType>(currentTickData.flags & RP_MOVETYPE_MASK); + botMoveType[bot] = replayMoveType; + int entityFlags = GetEntityFlags(client); + if (replayMoveType == MOVETYPE_WALK) + { + if (currentTickData.flags & RP_FL_ONGROUND) + { + SetEntityFlags(client, entityFlags | FL_ONGROUND); + botPaused[bot] = false; + // The bot is on the ground, so there must be a ground entity attributed to the bot. + int groundEnt = GetEntPropEnt(client, Prop_Send, "m_hGroundEntity"); + if (groundEnt == -1 && botJustTeleported[bot]) + { + SetEntPropFloat(client, Prop_Send, "m_flFallVelocity", 0.0); + float endPosition[3], mins[3], maxs[3]; + GetEntPropVector(client, Prop_Send, "m_vecMaxs", maxs); + GetEntPropVector(client, Prop_Send, "m_vecMins", mins); + endPosition = currentTickData.origin; + endPosition[2] -= 2.0; + TR_TraceHullFilter(currentTickData.origin, endPosition, mins, maxs, MASK_PLAYERSOLID, TraceEntityFilterPlayers); + + // This should always hit. + if (TR_DidHit()) + { + groundEnt = TR_GetEntityIndex(); + SetEntPropEnt(client, Prop_Data, "m_hGroundEntity", groundEnt); + } + } + } + else + { + botJustTeleported[bot] = false; + } + } + + if (currentTickData.flags & RP_UNDER_WATER) + { + SetEntityFlags(client, entityFlags | FL_INWATER); + } + + botSpeed[bot] = GetVectorHorizontalLength(currentTickData.velocity); + playbackTick[bot]++; + } +} + +// Set the bot client's GOKZ options, clan tag and name based on the loaded replay data +static void SetBotStuff(int bot) +{ + if (!botInGame[bot] || !botDataLoaded[bot]) + { + return; + } + + int client = botClient[bot]; + + // Set its movement options just in case it could negatively affect the playback + GOKZ_SetCoreOption(client, Option_Mode, botMode[bot]); + GOKZ_SetCoreOption(client, Option_Style, botStyle[bot]); + + // Clan tag and name + SetBotClanTag(bot); + SetBotName(bot); + + // Bot takes one tick after being put in server to be able to respawn. + RequestFrame(RequestFrame_SetBotStuff, GetClientUserId(client)); +} + +public void RequestFrame_SetBotStuff(int userid) +{ + int client = GetClientOfUserId(userid); + if (!client) + { + return; + } + int bot; + for (bot = 0; bot <= RP_MAX_BOTS; bot++) + { + if (botClient[bot] == client) + { + break; + } + else if (bot == RP_MAX_BOTS) + { + return; + } + } + // Set the bot's team based on if it's NUB or PRO + if (botReplayType[bot] == ReplayType_Run + && GOKZ_GetTimeTypeEx(botTeleportsUsed[bot]) == TimeType_Pro) + { + GOKZ_JoinTeam(client, CS_TEAM_CT, .forceBroadcast = true); + } + else + { + GOKZ_JoinTeam(client, CS_TEAM_CT, .forceBroadcast = true); + } + // Set bot weapons + // Always start by removing the pistol and knife + int currentPistol = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + if (currentPistol != -1) + { + RemovePlayerItem(client, currentPistol); + } + + int currentKnife = GetPlayerWeaponSlot(client, CS_SLOT_KNIFE); + if (currentKnife != -1) + { + RemovePlayerItem(client, currentKnife); + } + + char weaponName[128]; + // Give the bot the knife stored in the replay + /* + if (botKnife[bot] != 0) + { + CS_WeaponIDToAlias(CS_ItemDefIndexToID(botKnife[bot]), weaponName, sizeof(weaponName)); + Format(weaponName, sizeof(weaponName), "weapon_%s", weaponName); + GivePlayerItem(client, weaponName); + } + else + { + GivePlayerItem(client, "weapon_knife"); + } + */ + // We are currently not doing that, as it would require us to disable the + // FollowCSGOServerGuidelines failsafe if the bot has a non-standard knife. + GivePlayerItem(client, "weapon_knife"); + + // Give the bot the pistol stored in the replay + if (botWeapon[bot] != -1) + { + CS_WeaponIDToAlias(CS_ItemDefIndexToID(botWeapon[bot]), weaponName, sizeof(weaponName)); + Format(weaponName, sizeof(weaponName), "weapon_%s", weaponName); + GivePlayerItem(client, weaponName); + } + + botCurrentTeleport[bot] = 0; +} + +static void SetBotClanTag(int bot) +{ + char tag[MAX_NAME_LENGTH]; + + if (botReplayType[bot] == ReplayType_Run) + { + if (botCourse[bot] == 0) + { + // KZT PRO + FormatEx(tag, sizeof(tag), "%s %s", + gC_ModeNamesShort[botMode[bot]], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]); + } + else + { + // KZT B2 PRO + FormatEx(tag, sizeof(tag), "%s B%d %s", + gC_ModeNamesShort[botMode[bot]], botCourse[bot], gC_TimeTypeNames[GOKZ_GetTimeTypeEx(botTeleportsUsed[bot])]); + } + } + else if (botReplayType[bot] == ReplayType_Jump) + { + // KZT LJ + FormatEx(tag, sizeof(tag), "%s %s", + gC_ModeNamesShort[botMode[bot]], gC_JumpTypesShort[botJumpType[bot]]); + } + else + { + // KZT + FormatEx(tag, sizeof(tag), "%s", + gC_ModeNamesShort[botMode[bot]]); + } + + CS_SetClientClanTag(botClient[bot], tag); +} + +static void SetBotName(int bot) +{ + char name[MAX_NAME_LENGTH]; + + if (botReplayType[bot] == ReplayType_Run) + { + // DanZay (01:23.45) + FormatEx(name, sizeof(name), "%s (%s)", + botAlias[bot], GOKZ_FormatTime(botTime[bot])); + } + else if (botReplayType[bot] == ReplayType_Jump) + { + if (botJumpBlockDistance[bot] == 0) + { + // DanZay (291.44) + FormatEx(name, sizeof(name), "%s (%.2f)", + botAlias[bot], botJumpDistance[bot]); + } + else + { + // DanZay (291.44 on 289 block) + FormatEx(name, sizeof(name), "%s (%.2f on %d block)", + botAlias[bot], botJumpDistance[bot], botJumpBlockDistance[bot]); + } + } + else + { + // DanZay + FormatEx(name, sizeof(name), "%s", + botAlias[bot]); + } + + gB_HideNameChange = true; + SetClientName(botClient[bot], name); +} + +// Returns the number of bots that are currently replaying +static int GetBotsInUse() +{ + int botsInUse = 0; + for (int bot; bot < RP_MAX_BOTS; bot++) + { + if (botInGame[bot] && botDataLoaded[bot]) + { + botsInUse++; + } + } + return botsInUse; +} + +// Returns a bot that isn't currently replaying, or -1 if no unused bots found +static int GetUnusedBot() +{ + for (int bot = 0; bot < RP_MAX_BOTS; bot++) + { + if (!botInGame[bot]) + { + return bot; + } + } + return -1; +} + +static void PlaybackSkipToTick(int bot, int tick) +{ + if (botReplayVersion[bot] == 1) + { + // Load in the next tick + float repOrigin[3], repAngles[3]; + repOrigin[0] = playbackTickData[bot].Get(tick, 0); + repOrigin[1] = playbackTickData[bot].Get(tick, 1); + repOrigin[2] = playbackTickData[bot].Get(tick, 2); + repAngles[0] = playbackTickData[bot].Get(tick, 3); + repAngles[1] = playbackTickData[bot].Get(tick, 4); + + TeleportEntity(botClient[bot], repOrigin, repAngles, view_as<float>( { 0.0, 0.0, 0.0 } )); + } + else if (botReplayVersion[bot] == 2) + { + // Load in the next tick + ReplayTickData currentTickData; + playbackTickData[bot].GetArray(tick, currentTickData); + + TeleportEntity(botClient[bot], currentTickData.origin, currentTickData.angles, view_as<float>( { 0.0, 0.0, 0.0 } )); + + int direction = tick < playbackTick[bot] ? -1 : 1; + for (int i = playbackTick[bot]; i != tick; i += direction) + { + playbackTickData[bot].GetArray(i, currentTickData); + if (currentTickData.flags & RP_TELEPORT_TICK) + { + botCurrentTeleport[bot] += direction; + } + } + + #if defined DEBUG + PrintToServer("X %f \nY %f \nZ %f\nPitch %f\nYaw %f", currentTickData.origin[0], currentTickData.origin[1], currentTickData.origin[2], currentTickData.angles[0], currentTickData.angles[1]); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_WALK)) PrintToServer("MOVETYPE_WALK"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_LADDER)) PrintToServer("MOVETYPE_LADDER"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NOCLIP)) PrintToServer("MOVETYPE_NOCLIP"); + if(currentTickData.flags & RP_MOVETYPE_MASK == view_as<int>(MOVETYPE_NONE)) PrintToServer("MOVETYPE_NONE"); + + if(currentTickData.flags & RP_IN_ATTACK) PrintToServer("IN_ATTACK"); + if(currentTickData.flags & RP_IN_ATTACK2) PrintToServer("IN_ATTACK2"); + if(currentTickData.flags & RP_IN_JUMP) PrintToServer("IN_JUMP"); + if(currentTickData.flags & RP_IN_DUCK) PrintToServer("IN_DUCK"); + if(currentTickData.flags & RP_IN_FORWARD) PrintToServer("IN_FORWARD"); + if(currentTickData.flags & RP_IN_BACK) PrintToServer("IN_BACK"); + if(currentTickData.flags & RP_IN_LEFT) PrintToServer("IN_LEFT"); + if(currentTickData.flags & RP_IN_RIGHT) PrintToServer("IN_RIGHT"); + if(currentTickData.flags & RP_IN_MOVELEFT) PrintToServer("IN_MOVELEFT"); + if(currentTickData.flags & RP_IN_MOVERIGHT) PrintToServer("IN_MOVERIGHT"); + if(currentTickData.flags & RP_IN_RELOAD) PrintToServer("IN_RELOAD"); + if(currentTickData.flags & RP_IN_SPEED) PrintToServer("IN_SPEED"); + if(currentTickData.flags & RP_FL_ONGROUND) PrintToServer("FL_ONGROUND"); + if(currentTickData.flags & RP_FL_DUCKING ) PrintToServer("FL_DUCKING"); + if(currentTickData.flags & RP_FL_SWIM) PrintToServer("FL_SWIM"); + if(currentTickData.flags & RP_UNDER_WATER) PrintToServer("WATERLEVEL!=0"); + if(currentTickData.flags & RP_TELEPORT_TICK) PrintToServer("TELEPORT"); + if(currentTickData.flags & RP_TAKEOFF_TICK) PrintToServer("TAKEOFF"); + if(currentTickData.flags & RP_HIT_PERF) PrintToServer("PERF"); + if(currentTickData.flags & RP_SECONDARY_EQUIPPED) PrintToServer("SECONDARY_WEAPON_EQUIPPED"); + PrintToServer("=============================================================="); + #endif + } + + Movement_SetMovetype(botClient[bot], MOVETYPE_NOCLIP); + playbackTick[bot] = tick; +} + +static bool IsCurrentWeaponSecondary(int client) +{ + int activeWeaponEnt = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + int secondaryEnt = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + return activeWeaponEnt == secondaryEnt; +} + +static void MakePlayerSpectate(int client, int bot) +{ + GOKZ_JoinTeam(client, CS_TEAM_SPECTATOR); + SetEntProp(client, Prop_Send, "m_iObserverMode", 4); + SetEntPropEnt(client, Prop_Send, "m_hObserverTarget", bot); + + int clientUserID = GetClientUserId(client); + DataPack data = new DataPack(); + data.WriteCell(clientUserID); + data.WriteCell(GetClientUserId(bot)); + CreateTimer(0.1, Timer_UpdateBotName, GetClientUserId(bot)); + EnableReplayControls(client); +} + +public Action Timer_UpdateBotName(Handle timer, int botUID) +{ + Event e = CreateEvent("spec_target_updated"); + e.SetInt("userid", botUID); + e.Fire(); + return Plugin_Continue; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays/recording.sp b/sourcemod/scripting/gokz-replays/recording.sp new file mode 100644 index 0000000..babbd5e --- /dev/null +++ b/sourcemod/scripting/gokz-replays/recording.sp @@ -0,0 +1,990 @@ +/* + Bot replay recording logic and processes. + + Records data every time OnPlayerRunCmdPost is called. + If the player doesn't have their timer running, it keeps track + of the last 2 minutes of their actions. If a player is banned + while their timer isn't running, those 2 minutes are saved. + If the player has their timer running, the recording is done from + the beginning of the run. If the player can no longer beat their PB, + then the recording goes back to only keeping track of the last + two minutes. Upon beating their PB, a temporary binary file will be + written with a 'header' containing information about the run, + followed by the recorded tick data from OnPlayerRunCmdPost. + The binary file will be permanently locally saved on the server + if the run beats the server record. +*/ + +static float tickrate; +static int preAndPostRunTickCount; +static int maxCheaterReplayTicks; +static int recordingIndex[MAXPLAYERS + 1]; +static float playerSensitivity[MAXPLAYERS + 1]; +static float playerMYaw[MAXPLAYERS + 1]; +static bool isTeleportTick[MAXPLAYERS + 1]; +static ReplaySaveState replaySaveState[MAXPLAYERS + 1]; +static bool recordingPaused[MAXPLAYERS + 1]; +static bool postRunRecording[MAXPLAYERS + 1]; +static ArrayList recordedRecentData[MAXPLAYERS + 1]; +static ArrayList recordedRunData[MAXPLAYERS + 1]; +static ArrayList recordedPostRunData[MAXPLAYERS + 1]; +static Handle runningRunBreatherTimer[MAXPLAYERS + 1]; +static ArrayList runningJumpstatTimers[MAXPLAYERS + 1]; + +// =====[ EVENTS ]===== + +void OnMapStart_Recording() +{ + CreateReplaysDirectory(gC_CurrentMap); + tickrate = 1/GetTickInterval(); + preAndPostRunTickCount = RoundToZero(RP_PLAYBACK_BREATHER_TIME * tickrate); + maxCheaterReplayTicks = RoundToCeil(RP_MAX_CHEATER_REPLAY_LENGTH * tickrate); +} + +void OnClientPutInServer_Recording(int client) +{ + ClearClientRecordingState(client); +} + +void OnClientAuthorized_Recording(int client) +{ + // Apparently the client isn't valid yet here, so we can't check for that! + if(!IsFakeClient(client)) + { + // Create directory path for player if not exists + char replayPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, replayPath, sizeof(replayPath), "%s/%d", RP_DIRECTORY_JUMPS, GetSteamAccountID(client)); + if (!DirExists(replayPath)) + { + CreateDirectory(replayPath, 511); + } + BuildPath(Path_SM, replayPath, sizeof(replayPath), "%s/%d/%s", RP_DIRECTORY_JUMPS, GetSteamAccountID(client), RP_DIRECTORY_BLOCKJUMPS); + if (!DirExists(replayPath)) + { + CreateDirectory(replayPath, 511); + } + } +} + +void OnClientDisconnect_Recording(int client) +{ + // Stop exceptions if OnClientPutInServer was never ran for this client id. + // As long as the arrays aren't null we'll be fine. + if (runningJumpstatTimers[client] == null) + { + return; + } + + // Trigger all timers early + if(!IsFakeClient(client)) + { + if (runningRunBreatherTimer[client] != INVALID_HANDLE) + { + TriggerTimer(runningRunBreatherTimer[client], false); + } + + // We have to clone the array because the timer callback removes the timer + // from the array we're running over, and doing weird tricks is scary. + ArrayList timers = runningJumpstatTimers[client].Clone(); + for (int i = 0; i < timers.Length; i++) + { + Handle timer = timers.Get(i); + TriggerTimer(timer, false); + } + delete timers; + } + + ClearClientRecordingState(client); +} + +void OnPlayerRunCmdPost_Recording(int client, int buttons, int tickCount, const float vel[3], const int mouse[2]) +{ + if (!IsValidClient(client) || IsFakeClient(client) || !IsPlayerAlive(client) || recordingPaused[client]) + { + return; + } + + ReplayTickData tickData; + + Movement_GetOrigin(client, tickData.origin); + + tickData.mouse = mouse; + tickData.vel = vel; + Movement_GetVelocity(client, tickData.velocity); + Movement_GetEyeAngles(client, tickData.angles); + tickData.flags = EncodePlayerFlags(client, buttons, tickCount); + tickData.packetsPerSecond = GetClientAvgPackets(client, NetFlow_Incoming); + tickData.laggedMovementValue = GetEntPropFloat(client, Prop_Send, "m_flLaggedMovementValue"); + tickData.buttonsForced = GetEntProp(client, Prop_Data, "m_afButtonForced"); + + // HACK: Reset teleport tick marker. Too bad! + if (isTeleportTick[client]) + { + isTeleportTick[client] = false; + } + + if (replaySaveState[client] != ReplaySave_Disabled) + { + int runTick = GetArraySize(recordedRunData[client]); + if (runTick < RP_MAX_DURATION) + { + // Resize might fail if the timer exceed the max duration, + // as it is not guaranteed to allocate more than 1GB of contiguous memory, + // causing mass lag spikes that kick everyone out of the server. + // We can still attempt to save the rest of the recording though. + recordedRunData[client].Resize(runTick + 1); + recordedRunData[client].SetArray(runTick, tickData); + } + } + if (postRunRecording[client]) + { + int tick = GetArraySize(recordedPostRunData[client]); + if (tick < RP_MAX_DURATION) + { + recordedPostRunData[client].Resize(tick + 1); + recordedPostRunData[client].SetArray(tick, tickData); + } + } + + int tick = recordingIndex[client]; + if (recordedRecentData[client].Length < maxCheaterReplayTicks) + { + recordedRecentData[client].Resize(recordedRecentData[client].Length + 1); + recordingIndex[client] = recordingIndex[client] + 1 == maxCheaterReplayTicks ? 0 : recordingIndex[client] + 1; + } + else + { + recordingIndex[client] = RecordingIndexAdd(client, 1); + } + + recordedRecentData[client].SetArray(tick, tickData); +} + +Action GOKZ_OnTimerStart_Recording(int client) +{ + // Hack to fix an exception when starting the timer on the very + // first tick after loading the plugin. + if (recordedRecentData[client].Length == 0) + { + return Plugin_Handled; + } + + return Plugin_Continue; +} + +void GOKZ_OnTimerStart_Post_Recording(int client) +{ + replaySaveState[client] = ReplaySave_Local; + StartRunRecording(client); +} + +void GOKZ_OnTimerEnd_Recording(int client, int course, float time, int teleportsUsed) +{ + if (replaySaveState[client] == ReplaySave_Disabled) + { + return; + } + + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(course); + data.WriteFloat(time); + data.WriteCell(teleportsUsed); + data.WriteCell(replaySaveState[client]); + // The previous run breather still did not finish, end it now or + // we will start overwriting the data. + if (runningRunBreatherTimer[client] != INVALID_HANDLE) + { + TriggerTimer(runningRunBreatherTimer[client], false); + } + + replaySaveState[client] = ReplaySave_Disabled; + postRunRecording[client] = true; + + // Swap recordedRunData and recordedPostRunData. + // This lets new runs start immediately, before the post-run breather is + // finished recording. + ArrayList tmp = recordedPostRunData[client]; + recordedPostRunData[client] = recordedRunData[client]; + recordedRunData[client] = tmp; + recordedRunData[client].Clear(); + + runningRunBreatherTimer[client] = CreateTimer(RP_PLAYBACK_BREATHER_TIME, Timer_EndRecording, data); + if (runningRunBreatherTimer[client] == INVALID_HANDLE) + { + LogError("Could not create a timer so can't end the run replay recording"); + } +} + +public Action Timer_EndRecording(Handle timer, DataPack data) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int course = data.ReadCell(); + float time = data.ReadFloat(); + int teleportsUsed = data.ReadCell(); + ReplaySaveState saveState = data.ReadCell(); + delete data; + + // The client left after the run was done but before the post-run + // breather had the chance to finish. This should not happen, as we + // trigger all running timers on disconnect. + if (!IsValidClient(client)) + { + return Plugin_Stop; + } + + runningRunBreatherTimer[client] = INVALID_HANDLE; + postRunRecording[client] = false; + + if (gB_GOKZLocalDB && GOKZ_DB_IsCheater(client)) + { + // Replay might be submitted globally, but will not be saved locally. + saveState = ReplaySave_Temp; + } + + char path[PLATFORM_MAX_PATH]; + if (SaveRecordingOfRun(path, client, course, time, teleportsUsed, saveState == ReplaySave_Temp)) + { + Call_OnTimerEnd_Post(client, path, course, time, teleportsUsed); + } + else + { + Call_OnTimerEnd_Post(client, "", course, time, teleportsUsed); + } + + return Plugin_Stop; +} + +void GOKZ_OnPause_Recording(int client) +{ + PauseRecording(client); +} + +void GOKZ_OnResume_Recording(int client) +{ + ResumeRecording(client); +} + +void GOKZ_OnTimerStopped_Recording(int client) +{ + replaySaveState[client] = ReplaySave_Disabled; +} + +void GOKZ_OnCountedTeleport_Recording(int client) +{ + if (gB_NubRecordMissed[client]) + { + replaySaveState[client] = ReplaySave_Disabled; + } + + isTeleportTick[client] = true; +} + +void GOKZ_LR_OnRecordMissed_Recording(int client, int recordType) +{ + if (replaySaveState[client] == ReplaySave_Disabled) + { + return; + } + // If missed PRO record or both records, then can no longer beat a server record + if (recordType == RecordType_NubAndPro || recordType == RecordType_Pro) + { + replaySaveState[client] = ReplaySave_Temp; + } + + // If on a NUB run and missed NUB record, then can no longer beat a server record + // Otherwise wait to see if they teleport before stopping the recording + if (recordType == RecordType_Nub) + { + if (GOKZ_GetTeleportCount(client) > 0) + { + replaySaveState[client] = ReplaySave_Temp; + } + } +} + +public void GOKZ_LR_OnPBMissed(int client, float pbTime, int course, int mode, int style, int recordType) +{ + if (replaySaveState[client] == ReplaySave_Disabled) + { + return; + } + // If missed PRO record or both records, then can no longer beat PB + if (recordType == RecordType_NubAndPro || recordType == RecordType_Pro) + { + replaySaveState[client] = ReplaySave_Disabled; + } + + // If on a NUB run and missed NUB record, then can no longer beat PB + // Otherwise wait to see if they teleport before stopping the recording + if (recordType == RecordType_Nub) + { + if (GOKZ_GetTeleportCount(client) > 0) + { + replaySaveState[client] = ReplaySave_Disabled; + } + } +} + +void GOKZ_AC_OnPlayerSuspected_Recording(int client, ACReason reason) +{ + SaveRecordingOfCheater(client, reason); +} + +void GOKZ_DB_OnJumpstatPB_Recording(int client, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + DataPack data = new DataPack(); + data.WriteCell(GetClientUserId(client)); + data.WriteCell(jumptype); + data.WriteFloat(distance); + data.WriteCell(block); + data.WriteCell(strafes); + data.WriteFloat(sync); + data.WriteFloat(pre); + data.WriteFloat(max); + data.WriteCell(airtime); + + Handle timer = CreateTimer(RP_PLAYBACK_BREATHER_TIME, SaveJump, data); + if (timer != INVALID_HANDLE) + { + runningJumpstatTimers[client].Push(timer); + } + else + { + LogError("Could not create a timer so can't save jumpstat pb replay"); + } +} + +public Action SaveJump(Handle timer, DataPack data) +{ + data.Reset(); + int client = GetClientOfUserId(data.ReadCell()); + int jumptype = data.ReadCell(); + float distance = data.ReadFloat(); + int block = data.ReadCell(); + int strafes = data.ReadCell(); + float sync = data.ReadFloat(); + float pre = data.ReadFloat(); + float max = data.ReadFloat(); + int airtime = data.ReadCell(); + delete data; + + // The client left after the jump was done but before the post-jump + // breather had the chance to finish. This should not happen, as we + // trigger all running timers on disconnect. + if (!IsValidClient(client)) + { + return Plugin_Stop; + } + + RemoveFromRunningTimers(client, timer); + + SaveRecordingOfJump(client, jumptype, distance, block, strafes, sync, pre, max, airtime); + return Plugin_Stop; +} + + + +// =====[ PRIVATE ]===== + +static void ClearClientRecordingState(int client) +{ + recordingIndex[client] = 0; + playerSensitivity[client] = -1.0; + playerMYaw[client] = -1.0; + isTeleportTick[client] = false; + replaySaveState[client] = ReplaySave_Disabled; + recordingPaused[client] = false; + postRunRecording[client] = false; + runningRunBreatherTimer[client] = INVALID_HANDLE; + + if (recordedRecentData[client] == null) + recordedRecentData[client] = new ArrayList(sizeof(ReplayTickData)); + + if (recordedRunData[client] == null) + recordedRunData[client] = new ArrayList(sizeof(ReplayTickData)); + + if (recordedPostRunData[client] == null) + recordedPostRunData[client] = new ArrayList(sizeof(ReplayTickData)); + + if (runningJumpstatTimers[client] == null) + runningJumpstatTimers[client] = new ArrayList(); + + recordedRecentData[client].Clear(); + recordedRunData[client].Clear(); + recordedPostRunData[client].Clear(); + runningJumpstatTimers[client].Clear(); +} + +static void StartRunRecording(int client) +{ + if (IsFakeClient(client)) + { + return; + } + + QueryClientConVar(client, "sensitivity", SensitivityCheck, client); + QueryClientConVar(client, "m_yaw", MYAWCheck, client); + + DiscardRecording(client); + ResumeRecording(client); + + // Copy pre data + int index; + recordedRunData[client].Resize(preAndPostRunTickCount); + if (recordedRecentData[client].Length < preAndPostRunTickCount) + { + index = recordingIndex[client] - preAndPostRunTickCount; + } + else + { + index = RecordingIndexAdd(client, -preAndPostRunTickCount); + } + for (int i = 0; i < preAndPostRunTickCount; i++) + { + ReplayTickData tickData; + if (index < 0) + { + recordedRecentData[client].GetArray(0, tickData); + recordedRunData[client].SetArray(i, tickData); + index += 1; + } + else + { + recordedRecentData[client].GetArray(index, tickData); + recordedRunData[client].SetArray(i, tickData); + index = RecordingIndexAdd(client, -preAndPostRunTickCount + i + 1); + } + } +} + +static void DiscardRecording(int client) +{ + recordedRunData[client].Clear(); + Call_OnReplayDiscarded(client); +} + +static void PauseRecording(int client) +{ + recordingPaused[client] = true; +} + +static void ResumeRecording(int client) +{ + recordingPaused[client] = false; +} + +static bool SaveRecordingOfRun(char replayPath[PLATFORM_MAX_PATH], int client, int course, float time, int teleportsUsed, bool temp) +{ + // Prepare data + int timeType = GOKZ_GetTimeTypeEx(teleportsUsed); + + // Create and fill General Header + GeneralReplayHeader generalHeader; + FillGeneralHeader(generalHeader, client, ReplayType_Run, recordedPostRunData[client].Length); + + // Create and fill Run Header + RunReplayHeader runHeader; + runHeader.time = time; + runHeader.course = course; + runHeader.teleportsUsed = teleportsUsed; + + // Build path and create/overwrite associated file + FormatRunReplayPath(replayPath, sizeof(replayPath), course, generalHeader.mode, generalHeader.style, timeType, temp); + if (FileExists(replayPath)) + { + DeleteFile(replayPath); + } + else if (!temp) + { + AddToReplayInfoCache(course, generalHeader.mode, generalHeader.style, timeType); + SortReplayInfoCache(); + } + + File file = OpenFile(replayPath, "wb"); + if (file == null) + { + LogError("Failed to create/open replay file to write to: \"%s\".", replayPath); + return false; + } + + WriteGeneralHeader(file, generalHeader); + + // Write run header + file.WriteInt32(view_as<int>(runHeader.time)); + file.WriteInt8(runHeader.course); + file.WriteInt32(runHeader.teleportsUsed); + + WriteTickData(file, client, ReplayType_Run); + + delete file; + // If there is no plugin that wants to take over the replay file, we will delete it ourselves. + if (Call_OnReplaySaved(client, ReplayType_Run, gC_CurrentMap, course, timeType, time, replayPath, temp) == Plugin_Continue && temp) + { + DeleteFile(replayPath); + } + + return true; +} + +static bool SaveRecordingOfCheater(int client, ACReason reason) +{ + // Create and fill general header + GeneralReplayHeader generalHeader; + FillGeneralHeader(generalHeader, client, ReplayType_Cheater, recordedRecentData[client].Length); + + // Create and fill cheater header + CheaterReplayHeader cheaterHeader; + cheaterHeader.ACReason = reason; + + //Build path and create/overwrite associated file + char replayPath[PLATFORM_MAX_PATH]; + FormatCheaterReplayPath(replayPath, sizeof(replayPath), client, generalHeader.mode, generalHeader.style); + + File file = OpenFile(replayPath, "wb"); + if (file == null) + { + LogError("Failed to create/open replay file to write to: \"%s\".", replayPath); + return false; + } + + WriteGeneralHeader(file, generalHeader); + file.WriteInt8(view_as<int>(cheaterHeader.ACReason)); + WriteTickData(file, client, ReplayType_Cheater); + + delete file; + + return true; +} + +static bool SaveRecordingOfJump(int client, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + // Just cause I know how buggy jumpstats can be + int airtimeTicks = RoundToNearest((float(airtime) / GOKZ_DB_JS_AIRTIME_PRECISION) * tickrate); + if (airtimeTicks + 2 * preAndPostRunTickCount >= maxCheaterReplayTicks) + { + LogError("WARNING: Invalid airtime (this is probably a bugged jump, please report it!)."); + return false; + } + + // Create and fill general header + GeneralReplayHeader generalHeader; + FillGeneralHeader(generalHeader, client, ReplayType_Jump, 2 * preAndPostRunTickCount + airtimeTicks); + + // Create and fill jump header + JumpReplayHeader jumpHeader; + FillJumpHeader(jumpHeader, jumptype, distance, block, strafes, sync, pre, max, airtime); + + // Make sure the client is authenticated + if (GetSteamAccountID(client) == 0) + { + LogError("Failed to save jump, client is not authenticated."); + return false; + } + + // Build path and create/overwrite associated file + char replayPath[PLATFORM_MAX_PATH]; + if (block > 0) + { + FormatBlockJumpReplayPath(replayPath, sizeof(replayPath), client, block, jumpHeader.jumpType, generalHeader.mode, generalHeader.style); + } + else + { + FormatJumpReplayPath(replayPath, sizeof(replayPath), client, jumpHeader.jumpType, generalHeader.mode, generalHeader.style); + } + + File file = OpenFile(replayPath, "wb"); + if (file == null) + { + LogError("Failed to create/open replay file to write to: \"%s\".", replayPath); + delete file; + return false; + } + + WriteGeneralHeader(file, generalHeader); + WriteJumpHeader(file, jumpHeader); + WriteTickData(file, client, ReplayType_Jump, airtimeTicks); + + delete file; + + return true; +} + +static void FillGeneralHeader(GeneralReplayHeader generalHeader, int client, int replayType, int tickCount) +{ + // Prepare data + int mode = GOKZ_GetCoreOption(client, Option_Mode); + int style = GOKZ_GetCoreOption(client, Option_Style); + + // Fill general header + generalHeader.magicNumber = RP_MAGIC_NUMBER; + generalHeader.formatVersion = RP_FORMAT_VERSION; + generalHeader.replayType = replayType; + generalHeader.gokzVersion = GOKZ_VERSION; + generalHeader.mapName = gC_CurrentMap; + generalHeader.mapFileSize = gI_CurrentMapFileSize; + generalHeader.serverIP = FindConVar("hostip").IntValue; + generalHeader.timestamp = GetTime(); + GetClientName(client, generalHeader.playerAlias, sizeof(GeneralReplayHeader::playerAlias)); + generalHeader.playerSteamID = GetSteamAccountID(client); + generalHeader.mode = mode; + generalHeader.style = style; + generalHeader.playerSensitivity = playerSensitivity[client]; + generalHeader.playerMYaw = playerMYaw[client]; + generalHeader.tickrate = tickrate; + generalHeader.tickCount = tickCount; + generalHeader.equippedWeapon = GetPlayerWeaponSlotDefIndex(client, CS_SLOT_SECONDARY); + generalHeader.equippedKnife = GetPlayerWeaponSlotDefIndex(client, CS_SLOT_KNIFE); +} + +static void FillJumpHeader(JumpReplayHeader jumpHeader, int jumptype, float distance, int block, int strafes, float sync, float pre, float max, int airtime) +{ + jumpHeader.jumpType = jumptype; + jumpHeader.distance = distance; + jumpHeader.blockDistance = block; + jumpHeader.strafeCount = strafes; + jumpHeader.sync = sync; + jumpHeader.pre = pre; + jumpHeader.max = max; + jumpHeader.airtime = airtime; +} + +static void WriteGeneralHeader(File file, GeneralReplayHeader generalHeader) +{ + file.WriteInt32(generalHeader.magicNumber); + file.WriteInt8(generalHeader.formatVersion); + file.WriteInt8(generalHeader.replayType); + file.WriteInt8(strlen(generalHeader.gokzVersion)); + file.WriteString(generalHeader.gokzVersion, false); + file.WriteInt8(strlen(generalHeader.mapName)); + file.WriteString(generalHeader.mapName, false); + file.WriteInt32(generalHeader.mapFileSize); + file.WriteInt32(generalHeader.serverIP); + file.WriteInt32(generalHeader.timestamp); + file.WriteInt8(strlen(generalHeader.playerAlias)); + file.WriteString(generalHeader.playerAlias, false); + file.WriteInt32(generalHeader.playerSteamID); + file.WriteInt8(generalHeader.mode); + file.WriteInt8(generalHeader.style); + file.WriteInt32(view_as<int>(generalHeader.playerSensitivity)); + file.WriteInt32(view_as<int>(generalHeader.playerMYaw)); + file.WriteInt32(view_as<int>(generalHeader.tickrate)); + file.WriteInt32(generalHeader.tickCount); + file.WriteInt32(generalHeader.equippedWeapon); + file.WriteInt32(generalHeader.equippedKnife); +} + +static void WriteJumpHeader(File file, JumpReplayHeader jumpHeader) +{ + file.WriteInt8(jumpHeader.jumpType); + file.WriteInt32(view_as<int>(jumpHeader.distance)); + file.WriteInt32(jumpHeader.blockDistance); + file.WriteInt8(jumpHeader.strafeCount); + file.WriteInt32(view_as<int>(jumpHeader.sync)); + file.WriteInt32(view_as<int>(jumpHeader.pre)); + file.WriteInt32(view_as<int>(jumpHeader.max)); + file.WriteInt32((jumpHeader.airtime)); +} + +static void WriteTickData(File file, int client, int replayType, int airtime = 0) +{ + ReplayTickData tickData; + ReplayTickData prevTickData; + bool isFirstTick = true; + switch(replayType) + { + case ReplayType_Run: + { + for (int i = 0; i < recordedPostRunData[client].Length; i++) + { + recordedPostRunData[client].GetArray(i, tickData); + recordedPostRunData[client].GetArray(IntMax(0, i-1), prevTickData); + WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); + isFirstTick = false; + } + } + case ReplayType_Cheater: + { + for (int i = 0; i < recordedRecentData[client].Length; i++) + { + int rollingI = RecordingIndexAdd(client, i); + recordedRecentData[client].GetArray(rollingI, tickData); + recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData); + WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); + isFirstTick = false; + } + + } + case ReplayType_Jump: + { + int replayLength = 2 * preAndPostRunTickCount + airtime; + for (int i = 0; i < replayLength; i++) + { + int rollingI = RecordingIndexAdd(client, i - replayLength); + recordedRecentData[client].GetArray(rollingI, tickData); + recordedRecentData[client].GetArray(IntMax(0, i-1), prevTickData); + WriteTickDataToFile(file, isFirstTick, tickData, prevTickData); + isFirstTick = false; + } + } + } +} + +static void WriteTickDataToFile(File file, bool isFirstTick, ReplayTickData tickDataStruct, ReplayTickData prevTickDataStruct) +{ + any tickData[RP_V2_TICK_DATA_BLOCKSIZE]; + any prevTickData[RP_V2_TICK_DATA_BLOCKSIZE]; + TickDataToArray(tickDataStruct, tickData); + TickDataToArray(prevTickDataStruct, prevTickData); + + int deltaFlags = (1 << RPDELTA_DELTAFLAGS); + if (isFirstTick) + { + // NOTE: Set every bit to 1 until RP_V2_TICK_DATA_BLOCKSIZE. + deltaFlags = (1 << (RP_V2_TICK_DATA_BLOCKSIZE)) - 1; + } + else + { + // NOTE: Test tickData against prevTickData for differences. + for (int i = 1; i < sizeof(tickData); i++) + { + // If the bits in tickData[i] are different to prevTickData[i], then + // set the corresponding bitflag. + if (tickData[i] ^ prevTickData[i]) + { + deltaFlags |= (1 << i); + } + } + } + + file.WriteInt32(deltaFlags); + // NOTE: write only data that has changed since the previous tick. + for (int i = 1; i < sizeof(tickData); i++) + { + int currentFlag = (1 << i); + if (deltaFlags & currentFlag) + { + file.WriteInt32(tickData[i]); + } + } +} + +static void FormatRunReplayPath(char[] buffer, int maxlength, int course, int mode, int style, int timeType, bool tempPath) +{ + // Use GetEngineTime to prevent accidental replay overrides. + // Technically it would still be possible to override this file by accident, + // if somehow the server restarts to this exact map and course, + // and this function is run at the exact same time, but that is extremely unlikely. + // Also by then this file should have already been deleted. + char tempTimeString[32]; + Format(tempTimeString, sizeof(tempTimeString), "%f_", GetEngineTime()); + BuildPath(Path_SM, buffer, maxlength, + "%s/%s/%s%d_%s_%s_%s.%s", + tempPath ? RP_DIRECTORY_RUNS_TEMP : RP_DIRECTORY_RUNS, + gC_CurrentMap, + tempPath ? tempTimeString : "", + course, + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + gC_TimeTypeNames[timeType], + RP_FILE_EXTENSION); +} + +static void FormatCheaterReplayPath(char[] buffer, int maxlength, int client, int mode, int style) +{ + BuildPath(Path_SM, buffer, maxlength, + "%s/%d_%s_%d_%s_%s.%s", + RP_DIRECTORY_CHEATERS, + GetSteamAccountID(client), + gC_CurrentMap, + GetTime(), + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + RP_FILE_EXTENSION); +} + +static void FormatJumpReplayPath(char[] buffer, int maxlength, int client, int jumpType, int mode, int style) +{ + BuildPath(Path_SM, buffer, maxlength, + "%s/%d/%d_%s_%s.%s", + RP_DIRECTORY_JUMPS, + GetSteamAccountID(client), + jumpType, + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + RP_FILE_EXTENSION); +} + +static void FormatBlockJumpReplayPath(char[] buffer, int maxlength, int client, int block, int jumpType, int mode, int style) +{ + BuildPath(Path_SM, buffer, maxlength, + "%s/%d/%s/%d_%d_%s_%s.%s", + RP_DIRECTORY_JUMPS, + GetSteamAccountID(client), + RP_DIRECTORY_BLOCKJUMPS, + jumpType, + block, + gC_ModeNamesShort[mode], + gC_StyleNamesShort[style], + RP_FILE_EXTENSION); +} + +static int EncodePlayerFlags(int client, int buttons, int tickCount) +{ + int flags = 0; + MoveType movetype = Movement_GetMovetype(client); + int clientFlags = GetEntityFlags(client); + + flags = view_as<int>(movetype) & RP_MOVETYPE_MASK; + + SetKthBit(flags, 4, IsBitSet(buttons, IN_ATTACK)); + SetKthBit(flags, 5, IsBitSet(buttons, IN_ATTACK2)); + SetKthBit(flags, 6, IsBitSet(buttons, IN_JUMP)); + SetKthBit(flags, 7, IsBitSet(buttons, IN_DUCK)); + SetKthBit(flags, 8, IsBitSet(buttons, IN_FORWARD)); + SetKthBit(flags, 9, IsBitSet(buttons, IN_BACK)); + SetKthBit(flags, 10, IsBitSet(buttons, IN_LEFT)); + SetKthBit(flags, 11, IsBitSet(buttons, IN_RIGHT)); + SetKthBit(flags, 12, IsBitSet(buttons, IN_MOVELEFT)); + SetKthBit(flags, 13, IsBitSet(buttons, IN_MOVERIGHT)); + SetKthBit(flags, 14, IsBitSet(buttons, IN_RELOAD)); + SetKthBit(flags, 15, IsBitSet(buttons, IN_SPEED)); + SetKthBit(flags, 16, IsBitSet(buttons, IN_USE)); + SetKthBit(flags, 17, IsBitSet(buttons, IN_BULLRUSH)); + SetKthBit(flags, 18, IsBitSet(clientFlags, FL_ONGROUND)); + SetKthBit(flags, 19, IsBitSet(clientFlags, FL_DUCKING)); + SetKthBit(flags, 20, IsBitSet(clientFlags, FL_SWIM)); + + SetKthBit(flags, 21, GetEntProp(client, Prop_Data, "m_nWaterLevel") != 0); + + SetKthBit(flags, 22, isTeleportTick[client]); + SetKthBit(flags, 23, Movement_GetTakeoffTick(client) == tickCount); + SetKthBit(flags, 24, GOKZ_GetHitPerf(client)); + SetKthBit(flags, 25, IsCurrentWeaponSecondary(client)); + + return flags; +} + +// Function to set the bitNum bit in integer to value +static void SetKthBit(int &number, int offset, bool value) +{ + int intValue = value ? 1 : 0; + number |= intValue << offset; +} + +static bool IsBitSet(int number, int checkBit) +{ + return (number & checkBit) ? true : false; +} + +static int GetPlayerWeaponSlotDefIndex(int client, int slot) +{ + int ent = GetPlayerWeaponSlot(client, slot); + + // Nothing equipped in the slot + if (ent == -1) + { + return -1; + } + + return GetEntProp(ent, Prop_Send, "m_iItemDefinitionIndex"); +} + +static bool IsCurrentWeaponSecondary(int client) +{ + int activeWeaponEnt = GetEntPropEnt(client, Prop_Send, "m_hActiveWeapon"); + int secondaryEnt = GetPlayerWeaponSlot(client, CS_SLOT_SECONDARY); + return activeWeaponEnt == secondaryEnt; +} + +static void CreateReplaysDirectory(const char[] map) +{ + char path[PLATFORM_MAX_PATH]; + + // Create parent replay directory + BuildPath(Path_SM, path, sizeof(path), RP_DIRECTORY); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create maps parent replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_RUNS); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + + // Create maps replay directory + BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS, map); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create maps parent replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_RUNS_TEMP); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + + // Create maps replay directory + BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS_TEMP, map); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create cheaters replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_CHEATERS); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } + + // Create jumps parent replay directory + BuildPath(Path_SM, path, sizeof(path), "%s", RP_DIRECTORY_JUMPS); + if (!DirExists(path)) + { + CreateDirectory(path, 511); + } +} + +public void MYAWCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value) +{ + if (IsValidClient(client) && !IsFakeClient(client)) + { + playerMYaw[client] = StringToFloat(cvarValue); + } +} + +public void SensitivityCheck(QueryCookie cookie, int client, ConVarQueryResult result, const char[] cvarName, const char[] cvarValue, any value) +{ + if (IsValidClient(client) && !IsFakeClient(client)) + { + playerSensitivity[client] = StringToFloat(cvarValue); + } +} + +static int RecordingIndexAdd(int client, int offset) +{ + int index = recordingIndex[client] + offset; + if (index < 0) + { + index += recordedRecentData[client].Length; + } + return index % recordedRecentData[client].Length; +} + +static void RemoveFromRunningTimers(int client, Handle timerToRemove) +{ + int index = runningJumpstatTimers[client].FindValue(timerToRemove); + if (index != -1) + { + runningJumpstatTimers[client].Erase(index); + } +} diff --git a/sourcemod/scripting/gokz-replays/replay_cache.sp b/sourcemod/scripting/gokz-replays/replay_cache.sp new file mode 100644 index 0000000..83f36d0 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/replay_cache.sp @@ -0,0 +1,176 @@ +/* + Cached info about the map's available replay bots stored in an ArrayList. +*/ + + + +// =====[ PUBLIC ]===== + +// Adds a replay to the cache +void AddToReplayInfoCache(int course, int mode, int style, int timeType) +{ + int index = g_ReplayInfoCache.Length; + g_ReplayInfoCache.Resize(index + 1); + g_ReplayInfoCache.Set(index, course, 0); + g_ReplayInfoCache.Set(index, mode, 1); + g_ReplayInfoCache.Set(index, style, 2); + g_ReplayInfoCache.Set(index, timeType, 3); +} + +// Use this to sort the cache after finished adding to it +void SortReplayInfoCache() +{ + g_ReplayInfoCache.SortCustom(SortFunc_ReplayInfoCache); +} + +public int SortFunc_ReplayInfoCache(int index1, int index2, Handle array, Handle hndl) +{ + // Do not expect any indexes to be 'equal' + int replayInfo1[RP_CACHE_BLOCKSIZE], replayInfo2[RP_CACHE_BLOCKSIZE]; + g_ReplayInfoCache.GetArray(index1, replayInfo1); + g_ReplayInfoCache.GetArray(index2, replayInfo2); + + // Compare courses - lower course number goes first + if (replayInfo1[0] < replayInfo2[0]) + { + return -1; + } + else if (replayInfo1[0] > replayInfo2[0]) + { + return 1; + } + // Same course, so compare mode + else if (replayInfo1[1] < replayInfo2[1]) + { + return -1; + } + else if (replayInfo1[1] > replayInfo2[1]) + { + return 1; + } + // Same course and mode, so compare style + else if (replayInfo1[2] < replayInfo2[2]) + { + return -1; + } + else if (replayInfo1[2] > replayInfo2[2]) + { + return 1; + } + // Same course, mode and style so compare time type, assuming can't be identical + else if (replayInfo1[3] == TimeType_Pro) + { + return 1; + } + return -1; +} + + + +// =====[ EVENTS ]===== + +void OnMapStart_ReplayCache() +{ + if (g_ReplayInfoCache == null) + { + g_ReplayInfoCache = new ArrayList(RP_CACHE_BLOCKSIZE, 0); + } + else + { + g_ReplayInfoCache.Clear(); + } + + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "%s/%s", RP_DIRECTORY_RUNS, gC_CurrentMap); + DirectoryListing dir = OpenDirectory(path); + + // We want to find files that look like "0_KZT_NRM_PRO.rec" + char file[PLATFORM_MAX_PATH], pieces[4][16]; + int length, dotpos, course, mode, style, timeType; + + while (dir.GetNext(file, sizeof(file))) + { + // Some credit to Influx Timer - https://github.com/TotallyMehis/Influx-Timer + + // Check file extension + length = strlen(file); + dotpos = 0; + for (int i = 0; i < length; i++) + { + if (file[i] == '.') + { + dotpos = i; + } + } + if (!StrEqual(file[dotpos + 1], RP_FILE_EXTENSION, false)) + { + continue; + } + + // Remove file extension + Format(file, dotpos + 1, file); + + // Break down file name into pieces + if (ExplodeString(file, "_", pieces, sizeof(pieces), sizeof(pieces[])) != sizeof(pieces)) + { + continue; + } + + // Extract info from the pieces + course = StringToInt(pieces[0]); + mode = GetModeIDFromString(pieces[1]); + style = GetStyleIDFromString(pieces[2]); + timeType = GetTimeTypeIDFromString(pieces[3]); + if (!GOKZ_IsValidCourse(course) || mode == -1 || style == -1 || timeType == -1) + { + continue; + } + + // Add it to the cache + AddToReplayInfoCache(course, mode, style, timeType); + } + + SortReplayInfoCache(); + + delete dir; +} + + + +// =====[ PRIVATE ]===== + +static int GetModeIDFromString(const char[] mode) +{ + for (int modeID = 0; modeID < MODE_COUNT; modeID++) + { + if (StrEqual(mode, gC_ModeNamesShort[modeID], false)) + { + return modeID; + } + } + return -1; +} + +static int GetStyleIDFromString(const char[] style) +{ + for (int styleID = 0; styleID < STYLE_COUNT; styleID++) + { + if (StrEqual(style, gC_StyleNamesShort[styleID], false)) + { + return styleID; + } + } + return -1; +} + +static int GetTimeTypeIDFromString(const char[] timeType) +{ + for (int timeTypeID = 0; timeTypeID < TIMETYPE_COUNT; timeTypeID++) + { + if (StrEqual(timeType, gC_TimeTypeNames[timeTypeID], false)) + { + return timeTypeID; + } + } + return -1; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-replays/replay_menu.sp b/sourcemod/scripting/gokz-replays/replay_menu.sp new file mode 100644 index 0000000..94acd66 --- /dev/null +++ b/sourcemod/scripting/gokz-replays/replay_menu.sp @@ -0,0 +1,139 @@ +/* + Lets player select a replay bot to play back. +*/ + + + +static int selectedReplayMode[MAXPLAYERS + 1]; + + + +// =====[ PUBLIC ]===== + +void DisplayReplayModeMenu(int client) +{ + if (g_ReplayInfoCache.Length == 0) + { + GOKZ_PrintToChat(client, true, "%t", "No Replays Found (Map)"); + GOKZ_PlayErrorSound(client); + return; + } + + Menu menu = new Menu(MenuHandler_ReplayMode); + menu.SetTitle("%T", "Replay Menu (Mode) - Title", client, gC_CurrentMap); + GOKZ_MenuAddModeItems(client, menu, false); + menu.Display(client, MENU_TIME_FOREVER); +} + + + +// =====[ EVENTS ]===== + +public int MenuHandler_ReplayMode(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + selectedReplayMode[param1] = param2; + DisplayReplayMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + +public int MenuHandler_Replay(Menu menu, MenuAction action, int param1, int param2) +{ + if (action == MenuAction_Select) + { + char info[4]; + menu.GetItem(param2, info, sizeof(info)); + int replayIndex = StringToInt(info); + int replayInfo[RP_CACHE_BLOCKSIZE]; + g_ReplayInfoCache.GetArray(replayIndex, replayInfo); + + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), + "%s/%s/%d_%s_%s_%s.%s", + RP_DIRECTORY_RUNS, gC_CurrentMap, replayInfo[0], gC_ModeNamesShort[replayInfo[1]], gC_StyleNamesShort[replayInfo[2]], gC_TimeTypeNames[replayInfo[3]], RP_FILE_EXTENSION); + if (!FileExists(path)) + { + BuildPath(Path_SM, path, sizeof(path), + "%s/%d_%s_%s_%s.%s", + RP_DIRECTORY, gC_CurrentMap, replayInfo[0], gC_ModeNamesShort[replayInfo[1]], gC_StyleNamesShort[replayInfo[2]], gC_TimeTypeNames[replayInfo[3]], RP_FILE_EXTENSION); + if (!FileExists(path)) + { + LogError("Failed to load file: \"%s\".", path); + GOKZ_PrintToChat(param1, true, "%t", "Replay Menu - No File"); + return 0; + } + } + + LoadReplayBot(param1, path); + } + else if (action == MenuAction_Cancel) + { + DisplayReplayModeMenu(param1); + } + else if (action == MenuAction_End) + { + delete menu; + } + return 0; +} + + + +// =====[ PRIVATE ]===== + +static void DisplayReplayMenu(int client) +{ + Menu menu = new Menu(MenuHandler_Replay); + menu.SetTitle("%T", "Replay Menu - Title", client, gC_CurrentMap, gC_ModeNames[selectedReplayMode[client]]); + if (ReplayMenuAddItems(client, menu) > 0) + { + menu.Display(client, MENU_TIME_FOREVER); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "No Replays Found (Mode)", gC_ModeNames[selectedReplayMode[client]]); + GOKZ_PlayErrorSound(client); + DisplayReplayModeMenu(client); + } +} + +// Returns the number of replay menu items added +static int ReplayMenuAddItems(int client, Menu menu) +{ + int replaysAdded = 0; + int replayCount = g_ReplayInfoCache.Length; + int replayInfo[RP_CACHE_BLOCKSIZE]; + char temp[32], indexString[4]; + + menu.RemoveAllItems(); + + for (int i = 0; i < replayCount; i++) + { + IntToString(i, indexString, sizeof(indexString)); + g_ReplayInfoCache.GetArray(i, replayInfo); + if (replayInfo[1] != selectedReplayMode[client]) // Wrong mode! + { + continue; + } + + if (replayInfo[0] == 0) + { + FormatEx(temp, sizeof(temp), "Main %s", gC_TimeTypeNames[replayInfo[3]]); + } + else + { + FormatEx(temp, sizeof(temp), "Bonus %d %s", replayInfo[0], gC_TimeTypeNames[replayInfo[3]]); + } + menu.AddItem(indexString, temp, ITEMDRAW_DEFAULT); + + replaysAdded++; + } + + return replaysAdded; +}
\ No newline at end of file |
