/* * shavit's Timer - Replay Recorder * by: shavit * * This file is part of shavit's Timer. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 3.0, as published by the * Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . * */ #include #include #include #include #include #undef REQUIRE_PLUGIN #include #include #include #include public Plugin myinfo = { name = "[shavit] Replay Recorder", author = "shavit", description = "A replay recorder for shavit's bhop timer.", version = SHAVIT_VERSION, url = "https://github.com/shavitush/bhoptimer" } enum struct finished_run_info { int iSteamID; int style; float time; int jumps; int strafes; float sync; int track; float oldtime; float perfs; float avgvel; float maxvel; int timestamp; float fZoneOffset[2]; } bool gB_Late = false; char gS_Map[PLATFORM_MAX_PATH]; float gF_Tickrate = 0.0; int gI_Styles = 0; char gS_ReplayFolder[PLATFORM_MAX_PATH]; Convar gCV_Enabled = null; Convar gCV_PlaybackPostRunTime = null; Convar gCV_PlaybackPreRunTime = null; Convar gCV_PreRunAlways = null; Convar gCV_TimeLimit = null; Handle gH_ShouldSaveReplayCopy = null; Handle gH_OnReplaySaved = null; bool gB_RecordingEnabled[MAXPLAYERS+1]; // just a simple thing to prevent plugin reloads from recording half-replays // stuff related to postframes finished_run_info gA_FinishedRunInfo[MAXPLAYERS+1]; bool gB_GrabbingPostFrames[MAXPLAYERS+1]; Handle gH_PostFramesTimer[MAXPLAYERS+1]; int gI_PlayerFinishFrame[MAXPLAYERS+1]; // we use gI_PlayerFrames instead of grabbing gA_PlayerFrames.Length because the ArrayList is resized to handle 2s worth of extra frames to reduce how often we have to resize it int gI_PlayerFrames[MAXPLAYERS+1]; int gI_PlayerPrerunFrames[MAXPLAYERS+1]; ArrayList gA_PlayerFrames[MAXPLAYERS+1]; float gF_NextFrameTime[MAXPLAYERS+1]; int gI_HijackFrames[MAXPLAYERS+1]; float gF_HijackedAngles[MAXPLAYERS+1][2]; bool gB_HijackFramesKeepOnStart[MAXPLAYERS+1]; bool gB_ReplayPlayback = false; //#include forward void TickRate_OnTickRateChanged(float fOld, float fNew); public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { CreateNative("Shavit_GetClientFrameCount", Native_GetClientFrameCount); CreateNative("Shavit_GetPlayerPreFrames", Native_GetPlayerPreFrames); CreateNative("Shavit_GetReplayData", Native_GetReplayData); CreateNative("Shavit_HijackAngles", Native_HijackAngles); CreateNative("Shavit_SetReplayData", Native_SetReplayData); CreateNative("Shavit_SetPlayerPreFrames", Native_SetPlayerPreFrames); if (!FileExists("cfg/sourcemod/plugin.shavit-replay-recorder.cfg") && FileExists("cfg/sourcemod/plugin.shavit-replay.cfg")) { File source = OpenFile("cfg/sourcemod/plugin.shavit-replay.cfg", "r"); File destination = OpenFile("cfg/sourcemod/plugin.shavit-replay-recorder.cfg", "w"); if (source && destination) { char line[512]; while (!source.EndOfFile() && source.ReadLine(line, sizeof(line))) { destination.WriteLine("%s", line); } } delete destination; delete source; } RegPluginLibrary("shavit-replay-recorder"); gB_Late = late; return APLRes_Success; } public void OnPluginStart() { gH_ShouldSaveReplayCopy = CreateGlobalForward("Shavit_ShouldSaveReplayCopy", ET_Event, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell); gH_OnReplaySaved = CreateGlobalForward("Shavit_OnReplaySaved", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_Cell, Param_String, Param_Cell, Param_Cell, Param_Cell, Param_String); gCV_Enabled = new Convar("shavit_replay_recording_enabled", "1", "Enable replay bot functionality?", 0, true, 0.0, true, 1.0); gCV_PlaybackPostRunTime = new Convar("shavit_replay_postruntime", "1.5", "Time (in seconds) to record after a player enters the end zone.", 0, true, 0.0, true, 2.0); gCV_PreRunAlways = new Convar("shavit_replay_prerun_always", "1", "Record prerun frames outside the start zone?", 0, true, 0.0, true, 1.0); gCV_PlaybackPreRunTime = new Convar("shavit_replay_preruntime", "1.5", "Time (in seconds) to record before a player leaves start zone.", 0, true, 0.0, true, 2.0); gCV_TimeLimit = new Convar("shavit_replay_timelimit", "7200.0", "Maximum amount of time (in seconds) to allow saving to disk.\nDefault is 7200 (2 hours)\n0 - Disabled", 0, true, 0.0); Convar.AutoExecConfig(); gF_Tickrate = (1.0 / GetTickInterval()); gB_ReplayPlayback = LibraryExists("shavit-replay-playback"); if (gB_Late) { for (int i = 1; i <= MaxClients; i++) { if (IsValidClient(i) && !IsFakeClient(i)) { OnClientPutInServer(i); } } } } public void OnLibraryAdded(const char[] name) { if( StrEqual(name, "shavit-replay-playback")) { gB_ReplayPlayback = true; } } public void OnLibraryRemoved(const char[] name) { if (StrEqual(name, "shavit-replay-playback")) { gB_ReplayPlayback = false; } } bool LoadReplayConfig() { char sPath[PLATFORM_MAX_PATH]; BuildPath(Path_SM, sPath, PLATFORM_MAX_PATH, "configs/shavit-replay.cfg"); KeyValues kv = new KeyValues("shavit-replay"); if(!kv.ImportFromFile(sPath)) { delete kv; return false; } char sFolder[PLATFORM_MAX_PATH]; kv.GetString("replayfolder", sFolder, PLATFORM_MAX_PATH, "{SM}/data/replaybot"); if(StrContains(sFolder, "{SM}") != -1) { ReplaceString(sFolder, PLATFORM_MAX_PATH, "{SM}/", ""); BuildPath(Path_SM, sFolder, PLATFORM_MAX_PATH, "%s", sFolder); } strcopy(gS_ReplayFolder, PLATFORM_MAX_PATH, sFolder); delete kv; return true; } public void OnMapStart() { if (!LoadReplayConfig()) { SetFailState("Could not load the replay bots' configuration file. Make sure it exists (addons/sourcemod/configs/shavit-replay.cfg) and follows the proper syntax!"); } GetLowercaseMapName(gS_Map); Shavit_Replay_CreateDirectories(gS_ReplayFolder, gI_Styles); } public void OnClientPutInServer(int client) { ClearFrames(client); } public void OnClientDisconnect(int client) { if (gB_GrabbingPostFrames[client]) { FinishGrabbingPostFrames(client, gA_FinishedRunInfo[client]); } } public void OnClientDisconnect_Post(int client) { // This runs after shavit-misc has cloned the handle delete gA_PlayerFrames[client]; } public void TickRate_OnTickRateChanged(float fOld, float fNew) { gF_Tickrate = fNew; } void ClearFrames(int client) { delete gA_PlayerFrames[client]; gA_PlayerFrames[client] = new ArrayList(sizeof(frame_t)); gI_PlayerFrames[client] = 0; gF_NextFrameTime[client] = 0.0; gI_PlayerPrerunFrames[client] = 0; gI_PlayerFinishFrame[client] = 0; gI_HijackFrames[client] = 0; gB_HijackFramesKeepOnStart[client] = false; } public void Shavit_OnTimescaleChanged(int client, float oldtimescale, float newtimescale) { gF_NextFrameTime[client] = 0.0; } public Action Shavit_OnStart(int client) { gB_RecordingEnabled[client] = true; if (!gB_HijackFramesKeepOnStart[client]) { gI_HijackFrames[client] = 0; } if (gB_GrabbingPostFrames[client]) { FinishGrabbingPostFrames(client, gA_FinishedRunInfo[client]); } int iMaxPreFrames = RoundToFloor(gCV_PlaybackPreRunTime.FloatValue * gF_Tickrate / Shavit_GetStyleSettingFloat(Shavit_GetBhopStyle(client), "speed")); bool bInStart = Shavit_InsideZone(client, Zone_Start, Shavit_GetClientTrack(client)); if (bInStart) { int iFrameDifference = gI_PlayerFrames[client] - iMaxPreFrames; if (iFrameDifference > 0) { // For too many extra frames, we'll just shift the preframes to the start of the array. if (iFrameDifference > 100) { for (int i = iFrameDifference; i < gI_PlayerFrames[client]; i++) { gA_PlayerFrames[client].SwapAt(i, i-iFrameDifference); } gI_PlayerFrames[client] = iMaxPreFrames; } else // iFrameDifference isn't that bad, just loop through and erase. { while (iFrameDifference--) { gA_PlayerFrames[client].Erase(0); gI_PlayerFrames[client]--; } } } } else { if (!gCV_PreRunAlways.BoolValue) { ClearFrames(client); } } gI_PlayerPrerunFrames[client] = gI_PlayerFrames[client]; return Plugin_Continue; } public void Shavit_OnStop(int client) { if (gB_GrabbingPostFrames[client]) { FinishGrabbingPostFrames(client, gA_FinishedRunInfo[client]); } ClearFrames(client); } public Action Timer_PostFrames(Handle timer, int client) { gH_PostFramesTimer[client] = null; FinishGrabbingPostFrames(client, gA_FinishedRunInfo[client]); return Plugin_Stop; } void FinishGrabbingPostFrames(int client, finished_run_info info) { gB_GrabbingPostFrames[client] = false; delete gH_PostFramesTimer[client]; DoReplaySaverCallbacks(info.iSteamID, client, info.style, info.time, info.jumps, info.strafes, info.sync, info.track, info.oldtime, info.perfs, info.avgvel, info.maxvel, info.timestamp, info.fZoneOffset); } void DoReplaySaverCallbacks(int iSteamID, int client, int style, float time, int jumps, int strafes, float sync, int track, float oldtime, float perfs, float avgvel, float maxvel, int timestamp, float fZoneOffset[2]) { gA_PlayerFrames[client].Resize(gI_PlayerFrames[client]); bool isTooLong = (gCV_TimeLimit.FloatValue > 0.0 && time > gCV_TimeLimit.FloatValue); float length = gB_ReplayPlayback ? Shavit_GetReplayLength(style, track) : 999999999.0; bool isBestReplay = (length == 0.0 || time < length); Action action = Plugin_Continue; Call_StartForward(gH_ShouldSaveReplayCopy); Call_PushCell(client); Call_PushCell(style); Call_PushCell(time); Call_PushCell(jumps); Call_PushCell(strafes); Call_PushCell(sync); Call_PushCell(track); Call_PushCell(oldtime); Call_PushCell(perfs); Call_PushCell(avgvel); Call_PushCell(maxvel); Call_PushCell(timestamp); Call_PushCell(isBestReplay); Call_PushCell(isTooLong); Call_Finish(action); bool makeCopy = (action != Plugin_Continue); bool makeReplay = (isBestReplay && !isTooLong); if (!makeCopy && !makeReplay) { return; } char sName[MAX_NAME_LENGTH]; SanerGetClientName(client, sName); ReplaceString(sName, MAX_NAME_LENGTH, "#", "?"); int postframes = gI_PlayerFrames[client] - gI_PlayerFinishFrame[client]; char sPath[PLATFORM_MAX_PATH]; SaveReplay(style, track, time, iSteamID, sName, gI_PlayerPrerunFrames[client], gA_PlayerFrames[client], gI_PlayerFrames[client], postframes, timestamp, fZoneOffset, makeCopy, makeReplay, sPath, sizeof(sPath)); Call_StartForward(gH_OnReplaySaved); Call_PushCell(client); Call_PushCell(style); Call_PushCell(time); Call_PushCell(jumps); Call_PushCell(strafes); Call_PushCell(sync); Call_PushCell(track); Call_PushCell(oldtime); Call_PushCell(perfs); Call_PushCell(avgvel); Call_PushCell(maxvel); Call_PushCell(timestamp); Call_PushCell(isBestReplay); Call_PushCell(isTooLong); Call_PushCell(makeCopy); Call_PushString(sPath); Call_PushCell(gA_PlayerFrames[client]); Call_PushCell(gI_PlayerPrerunFrames[client]); Call_PushCell(postframes); Call_PushString(sName); Call_Finish(); ClearFrames(client); } public void Shavit_OnFinish(int client, int style, float time, int jumps, int strafes, float sync, int track, float oldtime, float perfs, float avgvel, float maxvel, int timestamp) { if (Shavit_IsPracticeMode(client) || !gCV_Enabled.BoolValue || gI_PlayerFrames[client] == 0) { return; } gI_PlayerFinishFrame[client] = gI_PlayerFrames[client]; float fZoneOffset[2]; fZoneOffset[0] = Shavit_GetZoneOffset(client, 0); fZoneOffset[1] = Shavit_GetZoneOffset(client, 1); if (gCV_PlaybackPostRunTime.FloatValue > 0.0) { finished_run_info info; info.iSteamID = GetSteamAccountID(client); info.style = style; info.time = time; info.jumps = jumps; info.strafes = strafes; info.sync = sync; info.track = track; info.oldtime = oldtime; info.perfs = perfs; info.avgvel = avgvel; info.maxvel = maxvel; info.timestamp = timestamp; info.fZoneOffset = fZoneOffset; gA_FinishedRunInfo[client] = info; gB_GrabbingPostFrames[client] = true; delete gH_PostFramesTimer[client]; gH_PostFramesTimer[client] = CreateTimer(gCV_PlaybackPostRunTime.FloatValue, Timer_PostFrames, client, TIMER_FLAG_NO_MAPCHANGE); } else { DoReplaySaverCallbacks(GetSteamAccountID(client), client, style, time, jumps, strafes, sync, track, oldtime, perfs, avgvel, maxvel, timestamp, fZoneOffset); } } void SaveReplay(int style, int track, float time, int steamid, char[] name, int preframes, ArrayList playerrecording, int iSize, int postframes, int timestamp, float fZoneOffset[2], bool saveCopy, bool saveWR, char[] sPath, int sPathLen) { char sTrack[4]; FormatEx(sTrack, 4, "_%d", track); File fWR = null; File fCopy = null; if (saveWR) { FormatEx(sPath, sPathLen, "%s/%d/%s%s.replay", gS_ReplayFolder, style, gS_Map, (track > 0)? sTrack:""); DeleteFile(sPath); fWR = OpenFile(sPath, "wb"); } if (saveCopy) { FormatEx(sPath, sPathLen, "%s/copy/%d_%d_%s.replay", gS_ReplayFolder, timestamp, steamid, gS_Map); DeleteFile(sPath); fCopy = OpenFile(sPath, "wb"); } if (fWR) { WriteReplayHeader(fWR, style, track, time, steamid, preframes, postframes, fZoneOffset, iSize, gF_Tickrate, gS_Map); } if (fCopy) { WriteReplayHeader(fCopy, style, track, time, steamid, preframes, postframes, fZoneOffset, iSize, gF_Tickrate, gS_Map); } WriteReplayFrames(playerrecording, iSize, fWR, fCopy); delete fWR; delete fCopy; } public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) { if (IsFakeClient(client) || !IsPlayerAlive(client)) { return; } if (!gA_PlayerFrames[client] || !gB_RecordingEnabled[client]) { return; } if (!gB_GrabbingPostFrames[client] && !(Shavit_ReplayEnabledStyle(Shavit_GetBhopStyle(client)) && Shavit_GetTimerStatus(client) == Timer_Running)) { return; } if ((gI_PlayerFrames[client] / gF_Tickrate) > gCV_TimeLimit.FloatValue) { if (gI_HijackFrames[client]) { gI_HijackFrames[client] = 0; } return; } float fTimescale = Shavit_GetClientTimescale(client); if (gF_NextFrameTime[client] > 0.0) { if (fTimescale != -1.0) { gF_NextFrameTime[client] -= fTimescale; } return; } if (gA_PlayerFrames[client].Length <= gI_PlayerFrames[client]) { // Add about two seconds worth of frames so we don't have to resize so often gA_PlayerFrames[client].Resize(gI_PlayerFrames[client] + (RoundToCeil(gF_Tickrate) * 2)); //PrintToChat(client, "resizing %d -> %d", gI_PlayerFrames[client], gA_PlayerFrames[client].Length); } frame_t aFrame; GetClientAbsOrigin(client, aFrame.pos); if (!gI_HijackFrames[client]) { float vecEyes[3]; GetClientEyeAngles(client, vecEyes); aFrame.ang[0] = vecEyes[0]; aFrame.ang[1] = vecEyes[1]; } else { aFrame.ang = gF_HijackedAngles[client]; --gI_HijackFrames[client]; } aFrame.buttons = buttons; aFrame.flags = GetEntityFlags(client); aFrame.mt = GetEntityMoveType(client); aFrame.mousexy = (mouse[0] & 0xFFFF) | ((mouse[1] & 0xFFFF) << 16); aFrame.vel = LimitMoveVelFloat(vel[0]) | (LimitMoveVelFloat(vel[1]) << 16); gA_PlayerFrames[client].SetArray(gI_PlayerFrames[client]++, aFrame, sizeof(frame_t)); if (fTimescale != -1.0) { gF_NextFrameTime[client] += (1.0 - fTimescale); } } stock int LimitMoveVelFloat(float vel) { int x = RoundToCeil(vel); return ((x < -666) ? -666 : ((x > 666) ? 666 : x)) & 0xFFFF; } public int Native_GetClientFrameCount(Handle handler, int numParams) { return gI_PlayerFrames[GetNativeCell(1)]; } public int Native_GetPlayerPreFrames(Handle handler, int numParams) { return gI_PlayerPrerunFrames[GetNativeCell(1)]; } public int Native_SetPlayerPreFrames(Handle handler, int numParams) { int client = GetNativeCell(1); int preframes = GetNativeCell(2); gI_PlayerPrerunFrames[client] = preframes; } public int Native_GetReplayData(Handle plugin, int numParams) { int client = GetNativeCell(1); bool cheapCloneHandle = view_as(GetNativeCell(2)); Handle cloned = null; if(gA_PlayerFrames[client] != null) { ArrayList frames = cheapCloneHandle ? gA_PlayerFrames[client] : gA_PlayerFrames[client].Clone(); frames.Resize(gI_PlayerFrames[client]); cloned = CloneHandle(frames, plugin); // set the calling plugin as the handle owner if (!cheapCloneHandle) { // Only hit for .Clone()'d handles. .Clone() != CloneHandle() CloseHandle(frames); } } return view_as(cloned); } public int Native_SetReplayData(Handle handler, int numParams) { int client = GetNativeCell(1); ArrayList data = view_as(GetNativeCell(2)); bool cheapCloneHandle = view_as(GetNativeCell(3)); if (gB_GrabbingPostFrames[client]) { FinishGrabbingPostFrames(client, gA_FinishedRunInfo[client]); } if (cheapCloneHandle) { data = view_as(CloneHandle(data)); } else { data = data.Clone(); } delete gA_PlayerFrames[client]; gA_PlayerFrames[client] = data; gI_PlayerFrames[client] = data.Length; } public int Native_HijackAngles(Handle handler, int numParams) { int client = GetNativeCell(1); gF_HijackedAngles[client][0] = view_as(GetNativeCell(2)); gF_HijackedAngles[client][1] = view_as(GetNativeCell(3)); int ticks = GetNativeCell(4); if (ticks == -1) { float latency = GetClientLatency(client, NetFlow_Both); if (latency > 0.0) { ticks = RoundToCeil(latency / GetTickInterval()) + 1; //PrintToChat(client, "%f %f %d", latency, GetTickInterval(), ticks); gI_HijackFrames[client] = ticks; } } else { gI_HijackFrames[client] = ticks; } gB_HijackFramesKeepOnStart[client] = (numParams < 5) ? false : view_as(GetNativeCell(5)); }