| 1 | /* (c) Redix and Sushi */ |
| 2 | |
| 3 | #include "race_demo.h" |
| 4 | |
| 5 | #include <base/system.h> |
| 6 | |
| 7 | #include <engine/shared/config.h> |
| 8 | #include <engine/storage.h> |
| 9 | |
| 10 | #include <game/client/gameclient.h> |
| 11 | #include <game/client/race.h> |
| 12 | #include <game/localization.h> |
| 13 | |
| 14 | #include <cctype> |
| 15 | #include <chrono> |
| 16 | |
| 17 | using namespace std::chrono_literals; |
| 18 | |
| 19 | const char *CRaceDemo::ms_pRaceDemoDir = "demos/auto/race" ; |
| 20 | |
| 21 | struct CDemoItem |
| 22 | { |
| 23 | char m_aName[128]; |
| 24 | int m_Time; |
| 25 | }; |
| 26 | |
| 27 | struct CDemoListParam |
| 28 | { |
| 29 | const CRaceDemo *m_pThis; |
| 30 | std::vector<CDemoItem> *m_pvDemos; |
| 31 | const char *m_pMap; |
| 32 | }; |
| 33 | |
| 34 | CRaceDemo::CRaceDemo() : |
| 35 | m_RaceState(RACE_NONE), m_RaceStartTick(-1), m_RecordStopTick(-1), m_Time(0) {} |
| 36 | |
| 37 | void CRaceDemo::GetPath(char *pBuf, int Size, int Time) const |
| 38 | { |
| 39 | const char *pMap = Client()->GetCurrentMap(); |
| 40 | |
| 41 | char aPlayerName[MAX_NAME_LENGTH]; |
| 42 | str_copy(dst&: aPlayerName, src: Client()->PlayerName()); |
| 43 | str_sanitize_filename(str: aPlayerName); |
| 44 | |
| 45 | if(Time < 0) |
| 46 | str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_tmp_%d.demo" , ms_pRaceDemoDir, pMap, pid()); |
| 47 | else if(g_Config.m_ClDemoName) |
| 48 | str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_%d.%03d_%s.demo" , ms_pRaceDemoDir, pMap, Time / 1000, Time % 1000, aPlayerName); |
| 49 | else |
| 50 | str_format(buffer: pBuf, buffer_size: Size, format: "%s/%s_%d.%03d.demo" , ms_pRaceDemoDir, pMap, Time / 1000, Time % 1000); |
| 51 | } |
| 52 | |
| 53 | void CRaceDemo::OnStateChange(int NewState, int OldState) |
| 54 | { |
| 55 | if(OldState == IClient::STATE_ONLINE) |
| 56 | StopRecord(); |
| 57 | } |
| 58 | |
| 59 | void CRaceDemo::OnNewSnapshot() |
| 60 | { |
| 61 | if(!GameClient()->m_GameInfo.m_Race || !g_Config.m_ClAutoRaceRecord || Client()->State() != IClient::STATE_ONLINE) |
| 62 | return; |
| 63 | |
| 64 | if(!GameClient()->m_Snap.m_pGameInfoObj || GameClient()->m_Snap.m_SpecInfo.m_Active || !GameClient()->m_Snap.m_pLocalCharacter || !GameClient()->m_Snap.m_pLocalPrevCharacter) |
| 65 | return; |
| 66 | |
| 67 | static int s_LastRaceTick = -1; |
| 68 | |
| 69 | bool RaceFlag = GameClient()->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_RACETIME; |
| 70 | bool ServerControl = RaceFlag && g_Config.m_ClRaceRecordServerControl; |
| 71 | int RaceTick = -GameClient()->m_Snap.m_pGameInfoObj->m_WarmupTimer; |
| 72 | |
| 73 | // start the demo |
| 74 | bool ForceStart = ServerControl && s_LastRaceTick != RaceTick && Client()->GameTick(Conn: g_Config.m_ClDummy) - RaceTick < Client()->GameTickSpeed(); |
| 75 | bool AllowRestart = (m_AllowRestart || ForceStart) && m_RaceStartTick + 10 * Client()->GameTickSpeed() < Client()->GameTick(Conn: g_Config.m_ClDummy); |
| 76 | if(m_RaceState == RACE_IDLE || m_RaceState == RACE_PREPARE || (m_RaceState == RACE_STARTED && AllowRestart)) |
| 77 | { |
| 78 | vec2 PrevPos = vec2(GameClient()->m_Snap.m_pLocalPrevCharacter->m_X, GameClient()->m_Snap.m_pLocalPrevCharacter->m_Y); |
| 79 | vec2 Pos = vec2(GameClient()->m_Snap.m_pLocalCharacter->m_X, GameClient()->m_Snap.m_pLocalCharacter->m_Y); |
| 80 | |
| 81 | if(ForceStart || (!ServerControl && GameClient()->RaceHelper()->IsStart(Prev: PrevPos, Pos))) |
| 82 | { |
| 83 | if(m_RaceState == RACE_STARTED) |
| 84 | Client()->RaceRecord_Stop(); |
| 85 | if(m_RaceState != RACE_PREPARE) // start recording again |
| 86 | { |
| 87 | GetPath(pBuf: m_aTmpFilename, Size: sizeof(m_aTmpFilename)); |
| 88 | Client()->RaceRecord_Start(pFilename: m_aTmpFilename); |
| 89 | } |
| 90 | m_RaceStartTick = Client()->GameTick(Conn: g_Config.m_ClDummy); |
| 91 | m_RaceState = RACE_STARTED; |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // start recording before the player passes the start line, so we can see some preparation steps |
| 96 | if(m_RaceState == RACE_NONE) |
| 97 | { |
| 98 | GetPath(pBuf: m_aTmpFilename, Size: sizeof(m_aTmpFilename)); |
| 99 | Client()->RaceRecord_Start(pFilename: m_aTmpFilename); |
| 100 | m_RaceStartTick = Client()->GameTick(Conn: g_Config.m_ClDummy); |
| 101 | m_RaceState = RACE_PREPARE; |
| 102 | } |
| 103 | |
| 104 | // stop recording if the player did not pass the start line after 20 seconds |
| 105 | if(m_RaceState == RACE_PREPARE && Client()->GameTick(Conn: g_Config.m_ClDummy) - m_RaceStartTick >= Client()->GameTickSpeed() * 20) |
| 106 | { |
| 107 | StopRecord(); |
| 108 | m_RaceState = RACE_IDLE; |
| 109 | } |
| 110 | |
| 111 | // stop the demo |
| 112 | if(m_RaceState == RACE_FINISHED && m_RecordStopTick <= Client()->GameTick(Conn: g_Config.m_ClDummy)) |
| 113 | StopRecord(Time: m_Time); |
| 114 | |
| 115 | s_LastRaceTick = RaceFlag ? RaceTick : -1; |
| 116 | } |
| 117 | |
| 118 | void CRaceDemo::OnReset() |
| 119 | { |
| 120 | StopRecord(); |
| 121 | } |
| 122 | |
| 123 | void CRaceDemo::OnShutdown() |
| 124 | { |
| 125 | StopRecord(); |
| 126 | } |
| 127 | |
| 128 | void CRaceDemo::OnMessage(int MsgType, void *pRawMsg) |
| 129 | { |
| 130 | // check for messages from server |
| 131 | if(MsgType == NETMSGTYPE_SV_KILLMSG) |
| 132 | { |
| 133 | CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; |
| 134 | if(pMsg->m_Victim == GameClient()->m_Snap.m_LocalClientId && Client()->RaceRecord_IsRecording()) |
| 135 | StopRecord(Time: m_Time); |
| 136 | } |
| 137 | else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM) |
| 138 | { |
| 139 | CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg; |
| 140 | for(int i = 0; i < MAX_CLIENTS; i++) |
| 141 | { |
| 142 | if(GameClient()->m_Teams.Team(ClientId: i) == pMsg->m_Team && i == GameClient()->m_Snap.m_LocalClientId && Client()->RaceRecord_IsRecording()) |
| 143 | StopRecord(Time: m_Time); |
| 144 | } |
| 145 | } |
| 146 | else if(MsgType == NETMSGTYPE_SV_CHAT) |
| 147 | { |
| 148 | CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; |
| 149 | if(pMsg->m_ClientId == -1 && m_RaceState == RACE_STARTED) |
| 150 | { |
| 151 | char aName[MAX_NAME_LENGTH]; |
| 152 | int Time = CRaceHelper::TimeFromFinishMessage(pStr: pMsg->m_pMessage, pNameBuf: aName, NameBufSize: sizeof(aName)); |
| 153 | if(Time > 0 && GameClient()->m_Snap.m_LocalClientId >= 0 && str_comp(a: aName, b: GameClient()->m_aClients[GameClient()->m_Snap.m_LocalClientId].m_aName) == 0) |
| 154 | { |
| 155 | m_RaceState = RACE_FINISHED; |
| 156 | m_RecordStopTick = Client()->GameTick(Conn: g_Config.m_ClDummy) + Client()->GameTickSpeed(); |
| 157 | m_Time = Time; |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | void CRaceDemo::OnMapLoad() |
| 164 | { |
| 165 | m_AllowRestart = false; |
| 166 | } |
| 167 | |
| 168 | void CRaceDemo::StopRecord(int Time) |
| 169 | { |
| 170 | if(Client()->RaceRecord_IsRecording()) |
| 171 | Client()->RaceRecord_Stop(); |
| 172 | |
| 173 | if(m_aTmpFilename[0] != '\0') |
| 174 | { |
| 175 | if(Time > 0 && CheckDemo(Time)) |
| 176 | { |
| 177 | // save file |
| 178 | char aNewFilename[512]; |
| 179 | GetPath(pBuf: aNewFilename, Size: sizeof(aNewFilename), Time: m_Time); |
| 180 | |
| 181 | Storage()->RenameFile(pOldFilename: m_aTmpFilename, pNewFilename: aNewFilename, Type: IStorage::TYPE_SAVE); |
| 182 | } |
| 183 | else // no new record |
| 184 | Storage()->RemoveFile(pFilename: m_aTmpFilename, Type: IStorage::TYPE_SAVE); |
| 185 | |
| 186 | m_aTmpFilename[0] = '\0'; |
| 187 | } |
| 188 | |
| 189 | m_Time = 0; |
| 190 | m_RaceState = RACE_NONE; |
| 191 | m_RaceStartTick = -1; |
| 192 | m_RecordStopTick = -1; |
| 193 | } |
| 194 | |
| 195 | struct SRaceDemoFetchUser |
| 196 | { |
| 197 | CRaceDemo *m_pThis; |
| 198 | CDemoListParam *m_pParam; |
| 199 | }; |
| 200 | |
| 201 | int CRaceDemo::RaceDemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser) |
| 202 | { |
| 203 | auto *pRealUser = (SRaceDemoFetchUser *)pUser; |
| 204 | auto *pParam = pRealUser->m_pParam; |
| 205 | int MapLen = str_length(str: pParam->m_pMap); |
| 206 | if(IsDir || !str_endswith(str: pInfo->m_pName, suffix: ".demo" ) || !str_startswith(str: pInfo->m_pName, prefix: pParam->m_pMap) || pInfo->m_pName[MapLen] != '_') |
| 207 | return 0; |
| 208 | |
| 209 | CDemoItem Item; |
| 210 | str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - 5); |
| 211 | |
| 212 | const char *pTime = Item.m_aName + MapLen + 1; |
| 213 | const char *pTEnd = pTime; |
| 214 | while(isdigit(*pTEnd) || *pTEnd == ' ' || *pTEnd == '.' || *pTEnd == ',') |
| 215 | pTEnd++; |
| 216 | |
| 217 | if(g_Config.m_ClDemoName) |
| 218 | { |
| 219 | char aPlayerName[MAX_NAME_LENGTH]; |
| 220 | str_copy(dst&: aPlayerName, src: pParam->m_pThis->Client()->PlayerName()); |
| 221 | str_sanitize_filename(str: aPlayerName); |
| 222 | |
| 223 | if(pTEnd[0] != '_' || str_comp(a: pTEnd + 1, b: aPlayerName) != 0) |
| 224 | return 0; |
| 225 | } |
| 226 | else if(pTEnd[0]) |
| 227 | return 0; |
| 228 | |
| 229 | Item.m_Time = CRaceHelper::TimeFromSecondsStr(pStr: pTime); |
| 230 | if(Item.m_Time > 0) |
| 231 | pParam->m_pvDemos->push_back(x: Item); |
| 232 | |
| 233 | if(time_get_nanoseconds() - pRealUser->m_pThis->m_RaceDemosLoadStartTime > 500ms) |
| 234 | { |
| 235 | pRealUser->m_pThis->GameClient()->m_Menus.RenderLoading(pCaption: Localize(pStr: "Loading race demo files" ), pContent: "" , IncreaseCounter: 0); |
| 236 | } |
| 237 | |
| 238 | return 0; |
| 239 | } |
| 240 | |
| 241 | bool CRaceDemo::CheckDemo(int Time) |
| 242 | { |
| 243 | std::vector<CDemoItem> vDemos; |
| 244 | CDemoListParam Param = {.m_pThis: this, .m_pvDemos: &vDemos, .m_pMap: Client()->GetCurrentMap()}; |
| 245 | m_RaceDemosLoadStartTime = time_get_nanoseconds(); |
| 246 | SRaceDemoFetchUser User; |
| 247 | User.m_pParam = &Param; |
| 248 | User.m_pThis = this; |
| 249 | Storage()->ListDirectoryInfo(Type: IStorage::TYPE_SAVE, pPath: ms_pRaceDemoDir, pfnCallback: RaceDemolistFetchCallback, pUser: &User); |
| 250 | |
| 251 | // loop through demo files |
| 252 | for(auto &Demo : vDemos) |
| 253 | { |
| 254 | if(Time >= Demo.m_Time) // found a better demo |
| 255 | return false; |
| 256 | |
| 257 | // delete old demo |
| 258 | char aFilename[IO_MAX_PATH_LENGTH]; |
| 259 | str_format(buffer: aFilename, buffer_size: sizeof(aFilename), format: "%s/%s.demo" , ms_pRaceDemoDir, Demo.m_aName); |
| 260 | Storage()->RemoveFile(pFilename: aFilename, Type: IStorage::TYPE_SAVE); |
| 261 | } |
| 262 | |
| 263 | return true; |
| 264 | } |
| 265 | |