From 24a688b2c36c4deff40c31b4633ae9a87d324395 Mon Sep 17 00:00:00 2001 From: GreYFoXGTi Date: Fri, 4 Feb 2011 19:25:04 +0200 Subject: [PATCH] Added AutoDemoRecord and ghost made by Race mod team, implemented to DDRace by noother --- .gitignore | 1 + src/engine/client.h | 7 + src/engine/client/client.cpp | 35 ++ src/engine/client/client.h | 7 + src/engine/shared/config_variables.h | 7 + src/engine/shared/storage.cpp | 1 + src/game/client/components/ghost.cpp | 615 ++++++++++++++++++++ src/game/client/components/ghost.h | 80 +++ src/game/client/components/menus.cpp | 11 + src/game/client/components/menus.h | 26 +- src/game/client/components/menus_ingame.cpp | 299 ++++++++++ src/game/client/components/race_demo.cpp | 202 +++++++ src/game/client/components/race_demo.h | 40 ++ src/game/client/gameclient.cpp | 19 +- src/game/client/gameclient.h | 6 + src/game/client/render.cpp | 14 +- src/game/client/render.h | 2 +- 17 files changed, 1364 insertions(+), 8 deletions(-) create mode 100644 src/game/client/components/ghost.cpp create mode 100644 src/game/client/components/ghost.h create mode 100644 src/game/client/components/race_demo.cpp create mode 100644 src/game/client/components/race_demo.h diff --git a/.gitignore b/.gitignore index 5519db982..b1f20ef57 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ DDRace-Server* *.user *.cmd .settings +*.opensdf diff --git a/src/engine/client.h b/src/engine/client.h index 2da2bd8b7..fbdc76dc0 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -131,6 +131,13 @@ public: virtual const char *LatestVersion() = 0; virtual bool ConnectionProblems() = 0; + //DDRace + virtual const char* GetCurrentMap() = 0; + virtual int GetCurrentMapCrc() = 0; + virtual const char* RaceRecordStart(const char *pFilename) = 0; + virtual void RaceRecordStop() = 0; + virtual bool DemoIsRecording() = 0; + virtual bool SoundInitFailed() = 0; }; diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 792e79eea..6451074ea 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -2354,3 +2354,38 @@ int main(int argc, const char **argv) // ignore_convention return 0; } + +const char* CClient::GetCurrentMap() +{ + return m_aCurrentMap; +} + +int CClient::GetCurrentMapCrc() +{ + return m_CurrentMapCrc; +} + +const char* CClient::RaceRecordStart(const char *pFilename) +{ + char aFilename[128]; + str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", m_aCurrentMap, pFilename); + + if(State() != STATE_ONLINE) + dbg_msg("demorec/record", "client is not online"); + else + m_DemoRecorder.Start(Storage(), m_pConsole, aFilename, GameClient()->NetVersion(), m_aCurrentMap, m_CurrentMapCrc, "client"); + + return m_aCurrentMap; +} + +void CClient::RaceRecordStop() +{ + if(m_DemoRecorder.IsRecording()) + m_DemoRecorder.Stop(); +} + +bool CClient::DemoIsRecording() +{ + return m_DemoRecorder.IsRecording(); +} + diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 9d3687706..fec51bf33 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -316,6 +316,13 @@ public: static void Con_Record(IConsole::IResult *pResult, void *pUserData, int ClientID); static void Con_StopRecord(IConsole::IResult *pResult, void *pUserData, int ClientID); + //DDRace + virtual const char* GetCurrentMap(); + virtual int GetCurrentMapCrc(); + virtual const char* RaceRecordStart(const char *pFilename); + virtual void RaceRecordStop(); + virtual bool DemoIsRecording(); + void RegisterCommands(); const char *DemoPlayer_Play(const char *pFilename, int StorageType); diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 3e7e6bdf3..92a686e80 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -193,4 +193,11 @@ MACRO_CONFIG_INT(BrFilterTestServer, br_filter_test_server, 0, 0, 2, CFGFLAG_SAV MACRO_CONFIG_INT(SvTeamAskTime, sv_team_ask_time, 10, 0, 9999, CFGFLAG_SERVER, "How much time the player has to accept or refuse", 3) MACRO_CONFIG_INT(SvAllowTeamLeader, sv_allow_team_leader, 1, 0, 1, CFGFLAG_SERVER, "Whether the admin allows teams to have leaders or not", 4) + +MACRO_CONFIG_INT(ClAutoRaceRecord, cl_auto_race_record, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Save the best demo of each race", -1) +MACRO_CONFIG_INT(ClDemoName, cl_demo_name, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Save the playername within the demo", -1) +MACRO_CONFIG_INT(ClRaceGhost, cl_race_ghost, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Enable ghost",-1) +MACRO_CONFIG_INT(ClRaceShowGhost, cl_race_show_ghost, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Show ghost",-1) +MACRO_CONFIG_INT(ClRaceSaveGhost, cl_race_save_ghost, 1, 0, 1, CFGFLAG_CLIENT|CFGFLAG_SAVE, "Save ghost",-1) + #endif diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index ffbd3aff5..a38d97916 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -63,6 +63,7 @@ public: fs_makedir(GetPath(TYPE_SAVE, "downloadedmaps", aPath, sizeof(aPath))); fs_makedir(GetPath(TYPE_SAVE, "demos", aPath, sizeof(aPath))); fs_makedir(GetPath(TYPE_SAVE, "demos/auto", aPath, sizeof(aPath))); + fs_makedir(GetPath(TYPE_SAVE, "ghosts", aPath, sizeof(aPath))); } return m_NumPaths ? 0 : 1; diff --git a/src/game/client/components/ghost.cpp b/src/game/client/components/ghost.cpp new file mode 100644 index 000000000..6d9a714a2 --- /dev/null +++ b/src/game/client/components/ghost.cpp @@ -0,0 +1,615 @@ +/* (c) Rajh, Redix and Sushi. */ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "skins.h" +#include "menus.h" +#include "ghost.h" + +/* +Note: +Freezing fucks up the ghost +the ghost isnt really sync +don't really get the client tick system for prediction +can used PrevChar and PlayerChar and it would be fluent and accurate but won't be predicted +so it will be affected by lags +*/ + +static const unsigned char gs_aHeaderMarker[8] = {'T', 'W', 'G', 'H', 'O', 'S', 'T', 0}; +static const unsigned char gs_ActVersion = 1; + +CGhost::CGhost() +{ + m_lGhosts.clear(); + m_CurPath.clear(); + m_CurPos = 0; + m_Recording = false; + m_Rendering = false; + m_RaceState = RACE_NONE; + m_NewRecord = false; + m_BestTime = -1; + m_StartRenderTick = -1; + m_StartRecordTick = -1; +} + +void CGhost::AddInfos(CNetObj_Character Player) +{ + if(!m_Recording) + return; + + + // Just to be sure it doesnt eat too much memory, the first test should be enough anyway + if((Client()->GameTick()-m_StartRecordTick) > Client()->GameTickSpeed()*60*13 || m_CurPath.size() > 50*15*60) + { + dbg_msg("ghost", "13 minutes elapsed. Stopping ghost record"); + StopRecord(); + m_CurPath.clear(); + return; + } + + + m_CurPath.add(Player); +} + +void CGhost::OnRender() +{ + // only for race + if(!m_pClient->m_IsRace || !g_Config.m_ClRaceGhost) + return; + + // Check if the race line is crossed then start the render of the ghost if one + if(m_RaceState != RACE_STARTED) { + bool start = false; + + std::list < int > Indices = m_pClient->Collision()->GetMapIndices(m_pClient->m_PredictedPrevChar.m_Pos, m_pClient->m_LocalCharacterPos); + if(!Indices.empty()) { + for(std::list < int >::iterator i = Indices.begin(); i != Indices.end(); i++) { + if(m_pClient->Collision()->GetTileIndex(*i) == TILE_BEGIN) start = true; + } + } else { + int CurrentIndex = m_pClient->Collision()->GetPureMapIndex(m_pClient->m_LocalCharacterPos); + if(m_pClient->Collision()->GetTileIndex(CurrentIndex) == TILE_BEGIN) start = true; + } + + if(start) { + dbg_msg("ghost", "race started"); + m_RaceState = RACE_STARTED; + StartRender(); + StartRecord(); + } + + } + + if(m_RaceState == RACE_FINISHED) + { + int OwnIndex = -1; + for(int i = 0; i < m_lGhosts.size(); i++) + if(m_lGhosts[i].m_ID == -1) + { + OwnIndex = i; + break; + } + + if(m_NewRecord) + { + dbg_msg("ghost", "new path saved"); + m_NewRecord = false; + CGhostList Ghost; + Ghost.m_ID = -1; + Ghost.m_GhostInfo = m_CurInfo; + Ghost.m_BestPath.clear(); + Ghost.m_BestPath = m_CurPath; + if(OwnIndex < 0) + m_lGhosts.add(Ghost); + else + m_lGhosts[OwnIndex] = Ghost; + + Save(); + } + StopRecord(); + StopRender(); + m_RaceState = RACE_NONE; + } + + CNetObj_Character Player = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Cur; + m_pClient->m_PredictedChar.Write(&Player); + + if(m_pClient->m_NewPredictedTick) + AddInfos(Player); + + // Play the ghost + if(!m_Rendering || !g_Config.m_ClRaceShowGhost) + return; + + m_CurPos = Client()->PredGameTick()-m_StartRenderTick; + + if(m_lGhosts.size() == 0 || m_CurPos < 0) + { + //dbg_msg("ghost", "Ghost path done"); + m_Rendering = false; + return; + } + + for(int i = 0; i < m_lGhosts.size(); i++) + { + RenderGhostHook(&m_lGhosts[i]); + RenderGhost(&m_lGhosts[i]); + } + +} + +void CGhost::RenderGhost(CGhostList *pGhost) +{ + if(m_CurPos >= pGhost->m_BestPath.size()) + return; + + CNetObj_Character Player = pGhost->m_BestPath[m_CurPos]; + CNetObj_Character Prev = pGhost->m_BestPath[m_CurPos]; + + if(m_CurPos > 0) + Prev = pGhost->m_BestPath[m_CurPos-1]; + + char aSkinName[64]; + IntsToStr(&pGhost->m_GhostInfo.m_Skin0, 6, aSkinName); + int SkinId = m_pClient->m_pSkins->Find(aSkinName); + if(SkinId < 0) + { + SkinId = m_pClient->m_pSkins->Find("default"); + if(SkinId < 0) + SkinId = 0; + } + + CTeeRenderInfo RenderInfo; + RenderInfo.m_ColorBody = m_pClient->m_pSkins->GetColorV4(pGhost->m_GhostInfo.m_ColorBody); + RenderInfo.m_ColorFeet = m_pClient->m_pSkins->GetColorV4(pGhost->m_GhostInfo.m_ColorFeet); + + if(pGhost->m_GhostInfo.m_UseCustomColor) + RenderInfo.m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_ColorTexture; + else + { + RenderInfo.m_Texture = m_pClient->m_pSkins->Get(SkinId)->m_OrgTexture; + RenderInfo.m_ColorBody = vec4(1,1,1,1); + RenderInfo.m_ColorFeet = vec4(1,1,1,1); + } + + RenderInfo.m_ColorBody.a = 0.5f; + RenderInfo.m_ColorFeet.a = 0.5f; + RenderInfo.m_Size = 64; + + float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, Client()->IntraGameTick())/256.0f; + vec2 Direction = GetDirection((int)(Angle*256.0f)); + vec2 Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->PredIntraGameTick()); + vec2 Vel = mix(vec2(Prev.m_VelX/256.0f, Prev.m_VelY/256.0f), vec2(Player.m_VelX/256.0f, Player.m_VelY/256.0f), Client()->PredIntraGameTick()); + + bool Stationary = Player.m_VelX <= 1 && Player.m_VelX >= -1; + bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y+16); + bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0); + + float WalkTime = fmod(absolute(Position.x), 100.0f)/100.0f; + CAnimState State; + State.Set(&g_pData->m_aAnimations[ANIM_BASE], 0); + + if(InAir) + State.Add(&g_pData->m_aAnimations[ANIM_INAIR], 0, 1.0f); + else if(Stationary) + State.Add(&g_pData->m_aAnimations[ANIM_IDLE], 0, 1.0f); + else if(!WantOtherDir) + State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f); + + if (Player.m_Weapon == WEAPON_GRENADE) + { + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle*pi*2+Angle); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); + + // normal weapons + int iw = clamp(Player.m_Weapon, 0, NUM_WEAPONS-1); + RenderTools()->SelectSprite(g_pData->m_Weapons.m_aId[iw].m_pSpriteBody, Direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0); + + vec2 Dir = Direction; + float Recoil = 0.0f; + // TODO: is this correct? + float a = (Client()->PredGameTick()-Player.m_AttackTick+Client()->PredIntraGameTick())/5.0f; + if(a < 1) + Recoil = sinf(a*pi); + + vec2 p = Position + Dir * g_pData->m_Weapons.m_aId[iw].m_Offsetx - Direction*Recoil*10.0f; + p.y += g_pData->m_Weapons.m_aId[iw].m_Offsety; + RenderTools()->DrawSprite(p.x, p.y, g_pData->m_Weapons.m_aId[iw].m_VisualSize); + Graphics()->QuadsEnd(); + } + + // Render ghost + RenderTools()->RenderTee(&State, &RenderInfo, 0, Direction, Position, true); +} + +void CGhost::RenderGhostHook(CGhostList *pGhost) +{ + if(m_CurPos >= pGhost->m_BestPath.size()) + return; + + CNetObj_Character Player = pGhost->m_BestPath[m_CurPos]; + CNetObj_Character Prev = pGhost->m_BestPath[m_CurPos]; + + if(m_CurPos > 0) + Prev = pGhost->m_BestPath[m_CurPos-1]; + + if (Prev.m_HookState<=0 || Player.m_HookState<=0) + return; + + float Angle = mix((float)Prev.m_Angle, (float)Player.m_Angle, Client()->IntraGameTick())/256.0f; + vec2 Direction = GetDirection((int)(Angle*256.0f)); + vec2 Pos = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->PredIntraGameTick()); + + vec2 HookPos = mix(vec2(Prev.m_HookX, Prev.m_HookY), vec2(Player.m_HookX, Player.m_HookY), Client()->PredIntraGameTick()); + float d = distance(Pos, HookPos); + vec2 Dir = normalize(Pos-HookPos); + + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GAME].m_Id); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(GetAngle(Dir)+pi); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); + + // render head + RenderTools()->SelectSprite(SPRITE_HOOK_HEAD); + IGraphics::CQuadItem QuadItem(HookPos.x, HookPos.y, 24, 16); + Graphics()->QuadsDraw(&QuadItem, 1); + + // render chain + RenderTools()->SelectSprite(SPRITE_HOOK_CHAIN); + IGraphics::CQuadItem Array[1024]; + int j = 0; + for(float f = 24; f < d && j < 1024; f += 24, j++) + { + vec2 p = HookPos + Dir*f; + Array[j] = IGraphics::CQuadItem(p.x, p.y, 24, 16); + } + + Graphics()->QuadsDraw(Array, j); + Graphics()->QuadsSetRotation(0); + Graphics()->QuadsEnd(); +} + +void CGhost::StartRecord() +{ + m_Recording = true; + m_CurPath.clear(); + CNetObj_ClientInfo *pInfo = (CNetObj_ClientInfo *) Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_CLIENTINFO, m_pClient->m_Snap.m_LocalCid); + m_CurInfo = *pInfo; + m_StartRecordTick = Client()->GameTick(); +} + +void CGhost::StopRecord() +{ + m_Recording = false; +} + +void CGhost::StartRender() +{ + m_CurPos = 0; + m_Rendering = true; + m_StartRenderTick = Client()->PredGameTick(); +} + +void CGhost::StopRender() +{ + m_Rendering = false; +} + +void CGhost::Save() +{ + if(!g_Config.m_ClRaceSaveGhost) + return; + + CGhostHeader Header; + + // check the player name + char aName[MAX_NAME_LENGTH]; + str_copy(aName, g_Config.m_PlayerName, sizeof(aName)); + for(int i = 0; i < MAX_NAME_LENGTH; i++) + { + if(!aName[i]) + break; + + if(aName[i] == '\\' || aName[i] == '/' || aName[i] == '|' || aName[i] == ':' || aName[i] == '*' || aName[i] == '?' || aName[i] == '<' || aName[i] == '>' || aName[i] == '"') + aName[i] = '%'; + } + + char aFilename[256]; + char aBuf[256]; + str_format(aFilename, sizeof(aFilename), "%s_%s_%.3f_%08x.gho", Client()->GetCurrentMap(), aName, m_BestTime, Client()->GetCurrentMapCrc()); + str_format(aBuf, sizeof(aBuf), "ghosts/%s", aFilename); + IOHANDLE File = Storage()->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!File) + return; + + // write header + int Crc = Client()->GetCurrentMapCrc(); + mem_zero(&Header, sizeof(Header)); + mem_copy(Header.m_aMarker, gs_aHeaderMarker, sizeof(Header.m_aMarker)); + Header.m_Version = gs_ActVersion; + IntsToStr(&m_CurInfo.m_Name0, 6, Header.m_aOwner); + str_copy(Header.m_aMap, Client()->GetCurrentMap(), sizeof(Header.m_aMap)); + Header.m_aCrc[0] = (Crc>>24)&0xff; + Header.m_aCrc[1] = (Crc>>16)&0xff; + Header.m_aCrc[2] = (Crc>>8)&0xff; + Header.m_aCrc[3] = (Crc)&0xff; + Header.m_Time = m_BestTime; + io_write(File, &Header, sizeof(Header)); + + // write client info + io_write(File, &m_CurInfo, sizeof(m_CurInfo)); + + // write data + int ItemsPerPackage = 500; // 500 ticks per package + int Num = m_CurPath.size(); + CNetObj_Character *Data = &m_CurPath[0]; + + while(Num) + { + int Items = min(Num, ItemsPerPackage); + Num -= Items; + + char aBuffer[100*500]; + char aBuffer2[100*500]; + unsigned char aSize[4]; + + int Size = sizeof(CNetObj_Character)*Items; + mem_copy(aBuffer2, Data, Size); + Data += Items; + + Size = CVariableInt::Compress(aBuffer2, Size, aBuffer); + Size = CNetBase::Compress(aBuffer, Size, aBuffer2, sizeof(aBuffer2)); + + aSize[0] = (Size>>24)&0xff; + aSize[1] = (Size>>16)&0xff; + aSize[2] = (Size>>8)&0xff; + aSize[3] = (Size)&0xff; + + io_write(File, aSize, sizeof(aSize)); + io_write(File, aBuffer2, Size); + } + + io_close(File); + + // remove old ghost from list + for(int i = 0; i < m_pClient->m_pMenus->m_lGhosts.size(); i++) + { + CMenus::CGhostItem TmpItem = m_pClient->m_pMenus->m_lGhosts[i]; + if(TmpItem.m_ID == -1) + { + char aFile[256]; + str_format(aFile, sizeof(aFile), "ghosts/%s", TmpItem.m_aFilename); + Storage()->RemoveFile(aFile, IStorage::TYPE_SAVE); + m_pClient->m_pMenus->m_lGhosts.remove_index(i); + break; // TODO: remove other ghosts? + } + } + + // add new ghost to ghost list + CMenus::CGhostItem Item; + str_copy(Item.m_aFilename, aFilename, sizeof(Item.m_aFilename)); + str_copy(Item.m_aPlayer, Header.m_aOwner, sizeof(Item.m_aPlayer)); + Item.m_Time = m_BestTime; + Item.m_Active = true; + Item.m_ID = -1; + m_pClient->m_pMenus->m_lGhosts.add(Item); +} + +bool CGhost::GetHeader(IOHANDLE *pFile, CGhostHeader *pHeader) +{ + if(!*pFile) + return 0; + + CGhostHeader Header; + io_read(*pFile, &Header, sizeof(Header)); + + *pHeader = Header; + + if(mem_comp(Header.m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) != 0) + return 0; + + if(Header.m_Version > gs_ActVersion) + return 0; + + int Crc = (Header.m_aCrc[0]<<24) | (Header.m_aCrc[1]<<16) | (Header.m_aCrc[2]<<8) | (Header.m_aCrc[3]); + if(str_comp(Header.m_aMap, Client()->GetCurrentMap()) != 0 || Crc != Client()->GetCurrentMapCrc()) + return 0; + + return 1; +} + +bool CGhost::GetInfo(const char* pFilename, CGhostHeader *pHeader) +{ + char aFilename[256]; + str_format(aFilename, sizeof(aFilename), "ghosts/%s", pFilename); + IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_READ, IStorage::TYPE_SAVE); + if(!File) + return 0; + + bool Check = GetHeader(&File, pHeader); + io_close(File); + + return Check; +} + +void CGhost::Load(const char* pFilename, int ID) +{ + char aFilename[256]; + str_format(aFilename, sizeof(aFilename), "ghosts/%s", pFilename); + IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_READ, IStorage::TYPE_SAVE); + if(!File) + return; + + // read header + CGhostHeader Header; + if(!GetHeader(&File, &Header)) + { + io_close(File); + return; + } + + if(ID == -1) + m_BestTime = Header.m_Time; + + // create ghost + CGhostList Ghost; + Ghost.m_ID = ID; + + // read client info + io_read(File, &Ghost.m_GhostInfo, sizeof(Ghost.m_GhostInfo)); + + // read data + Ghost.m_BestPath.clear(); + + while(1) + { + static char aCompresseddata[100*500]; + static char aDecompressed[100*500]; + static char aData[100*500]; + + unsigned char aSize[4]; + if(io_read(File, aSize, sizeof(aSize)) != sizeof(aSize)) + break; + int Size = (aSize[0]<<24) | (aSize[1]<<16) | (aSize[2]<<8) | aSize[3]; + + if(io_read(File, aCompresseddata, Size) != (unsigned)Size) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error reading chunk"); + break; + } + + Size = CNetBase::Decompress(aCompresseddata, Size, aDecompressed, sizeof(aDecompressed)); + if(Size < 0) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during network decompression"); + break; + } + + Size = CVariableInt::Decompress(aDecompressed, Size, aData); + if(Size < 0) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "ghost", "error during intpack decompression"); + break; + } + + CNetObj_Character *Tmp = (CNetObj_Character*)aData; + for(int i = 0; i < (signed)(Size/sizeof(CNetObj_Character)); i++) + { + Ghost.m_BestPath.add(*Tmp); + Tmp++; + } + } + + io_close(File); + + m_lGhosts.add(Ghost); +} + +void CGhost::Unload(int ID) +{ + for(int i = 0; i < m_lGhosts.size(); i++) + { + if(m_lGhosts[i].m_ID == ID) + { + m_lGhosts.remove_index_fast(i); + break; + } + } +} + +void CGhost::ConGPlay(IConsole::IResult *pResult, void *pUserData, int ClientId) +{ + ((CGhost *)pUserData)->StartRender(); +} + +void CGhost::OnConsoleInit() +{ + Console()->Register("gplay","", CFGFLAG_CLIENT, ConGPlay, this, "", -1); +} + +void CGhost::OnMessage(int MsgType, void *pRawMsg) +{ + if(!g_Config.m_ClRaceGhost || m_pClient->m_Snap.m_Spectate) + return; + + // only for race + if(!m_pClient->m_IsRace) + return; + + // check for messages from server + if(MsgType == NETMSGTYPE_SV_KILLMSG) + { + CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; + if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalCid) + { + OnReset(); + } + } + else if(MsgType == NETMSGTYPE_SV_CHAT) + { + CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; + if(pMsg->m_Cid == -1 && m_RaceState == RACE_STARTED) + { + const char* pMessage = pMsg->m_pMessage; + + int Num = 0; + while(str_comp_num(pMessage, " finished in: ", 14)) + { + pMessage++; + Num++; + if(!pMessage[0]) + return; + } + + // store the name + char aName[64]; + str_copy(aName, pMsg->m_pMessage, Num+1); + + // prepare values and state for saving + int Minutes; + float Seconds; + if(!str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName) && sscanf(pMessage, " finished in: %d minute(s) %f", &Minutes, &Seconds) == 2) + { + m_RaceState = RACE_FINISHED; + if(m_Recording) + { + float CurTime = Minutes*60 + Seconds; + if(CurTime < m_BestTime || m_BestTime == -1) + { + m_NewRecord = true; + m_BestTime = CurTime; + } + } + } + } + } +} + +void CGhost::OnReset() +{ + StopRecord(); + StopRender(); + m_RaceState = RACE_NONE; + m_NewRecord = false; + m_CurPath.clear(); + m_StartRenderTick = -1; +} + +void CGhost::OnMapLoad() +{ + OnReset(); + m_BestTime = -1; + m_lGhosts.clear(); + m_pClient->m_pMenus->GhostlistPopulate(); +} diff --git a/src/game/client/components/ghost.h b/src/game/client/components/ghost.h new file mode 100644 index 000000000..e5db93e6f --- /dev/null +++ b/src/game/client/components/ghost.h @@ -0,0 +1,80 @@ +/* (c) Rajh, Redix and Sushi. */ + +#ifndef GAME_CLIENT_COMPONENTS_GHOST_H +#define GAME_CLIENT_COMPONENTS_GHOST_H + +#include + +class CGhost : public CComponent +{ +public: + struct CGhostHeader + { + unsigned char m_aMarker[8]; + unsigned char m_Version; + char m_aOwner[MAX_NAME_LENGTH]; + char m_aMap[64]; + unsigned char m_aCrc[4]; + float m_Time; + }; + +private: + struct CGhostList + { + int m_ID; + CNetObj_ClientInfo m_GhostInfo; + array m_BestPath; + }; + array m_lGhosts; + + array m_CurPath; + + CNetObj_ClientInfo m_CurInfo; + + int m_StartRenderTick; + int m_StartRecordTick; + int m_CurPos; + bool m_Recording; + bool m_Rendering; + int m_RaceState; + float m_BestTime; + bool m_NewRecord; + + enum + { + RACE_NONE = 0, + RACE_STARTED, + RACE_FINISHED, + }; + + void AddInfos(CNetObj_Character Player); + + void StartRecord(); + void StopRecord(); + void StartRender(); + void StopRender(); + void RenderGhost(CGhostList *pGhost); + void RenderGhostHook(CGhostList *pGhost); + + bool GetHeader(IOHANDLE *pFile, CGhostHeader *pHeader); + + void Save(); + + static void ConGPlay(IConsole::IResult *pResult, void *pUserData, int ClientId); + +public: + CGhost(); + + virtual void OnRender(); + virtual void OnConsoleInit(); + virtual void OnReset(); + virtual void OnMessage(int MsgType, void *pRawMsg); + virtual void OnMapLoad(); + + void Load(const char* pFilename, int ID); + void Unload(int ID); + + bool GetInfo(const char* pFilename, CGhostHeader *pHeader); +}; + +#endif diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 50ccf229f..1065bbf48 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -552,6 +552,15 @@ int CMenus::RenderMenubar(CUIRect r) static int s_ServerInfoButton=0; if(DoButton_MenuTab(&s_ServerInfoButton, Localize("Server info"), m_ActivePage==PAGE_SERVER_INFO, &Button, CUI::CORNER_T)) NewPage = PAGE_SERVER_INFO; + + if(m_pClient->m_IsRace) + { + Box.VSplitLeft(4.0f, 0, &Box); + Box.VSplitLeft(100.0f, &Button, &Box); + static int s_GhostButton=0; + if(DoButton_MenuTab(&s_GhostButton, Localize("Ghost"), m_ActivePage==PAGE_GHOST, &Button, CUI::CORNER_T)) + NewPage = PAGE_GHOST; + } Box.VSplitLeft(4.0f, 0, &Box); Box.VSplitLeft(140.0f, &Button, &Box); @@ -788,6 +797,8 @@ int CMenus::Render() RenderGame(MainView); else if(m_GamePage == PAGE_SERVER_INFO) RenderServerInfo(MainView); + else if(m_GamePage == PAGE_GHOST) + RenderGhost(MainView); else if(m_GamePage == PAGE_CALLVOTE) RenderServerControl(MainView); else if(m_GamePage == PAGE_BROWSER) diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 5812d1000..2c137d2e8 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -107,6 +107,7 @@ class CMenus : public CComponent PAGE_NEWS=1, PAGE_GAME, PAGE_SERVER_INFO, + PAGE_GHOST, PAGE_CALLVOTE, PAGE_INTERNET, PAGE_LAN, @@ -180,16 +181,16 @@ class CMenus : public CComponent str_comp_filenames(m_aFilename, Other.m_aFilename) < 0; } }; - sorted_array m_lDemos; char m_aCurrentDemoFolder[256]; int m_DemolistSelectedIndex; bool m_DemolistSelectedIsDir; int m_DemolistStorageType; void DemolistOnUpdate(bool Reset); - void DemolistPopulate(); static void DemolistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser); + static void GhostlistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser); + // found in menus.cpp int Render(); //void render_background(); @@ -207,6 +208,7 @@ class CMenus : public CComponent void RenderServerControl(CUIRect MainView); void RenderServerControlKick(CUIRect MainView); void RenderServerControlServer(CUIRect MainView); + void RenderGhost(CUIRect MainView); void RenderInGameBrowser(CUIRect MainView); // found in menus_browser.cpp @@ -250,5 +252,25 @@ public: //DDRace int DoButton_CheckBox_DontCare(const void *pID, const char *pText, int Checked, const CUIRect *pRect); + sorted_array m_lDemos; + void DemolistPopulate(); + + // ghost + struct CGhostItem + { + char m_aFilename[256]; + char m_aPlayer[MAX_NAME_LENGTH]; + + float m_Time; + + bool m_Active; + int m_ID; + + bool operator<(const CGhostItem &Other) { return m_Time < Other.m_Time; } + }; + + sorted_array m_lGhosts; + + void GhostlistPopulate(); }; #endif diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index 0d68493ad..f53dd9203 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -20,6 +20,10 @@ #include "menus.h" #include "motd.h" #include "voting.h" +#include +#include +#include +#include "ghost.h" void CMenus::RenderGame(CUIRect MainView) { @@ -514,3 +518,298 @@ void CMenus::RenderInGameBrowser(CUIRect MainView) RenderServerbrowser(MainView); return; } + +// ghost stuff +void CMenus::GhostlistFetchCallback(const char *pName, int IsDir, int StorageType, void *pUser) +{ + CMenus *pSelf = (CMenus *)pUser; + int Length = str_length(pName); + if((pName[0] == '.' && (pName[1] == 0 || + (pName[1] == '.' && pName[2] == 0))) || + (!IsDir && (Length < 4 || str_comp(pName+Length-4, ".gho")))) + return; + + CGhost::CGhostHeader Header; + if(!pSelf->m_pClient->m_pGhost->GetInfo(pName, &Header)) + return; + + CGhostItem Item; + str_copy(Item.m_aFilename, pName, sizeof(Item.m_aFilename)); + str_copy(Item.m_aPlayer, Header.m_aOwner, sizeof(Item.m_aPlayer)); + Item.m_Time = Header.m_Time; + + Item.m_Active = false; + Item.m_ID = pSelf->m_lGhosts.size(); + + pSelf->m_lGhosts.add(Item); +} + +void CMenus::GhostlistPopulate() +{ + m_lGhosts.clear(); + Storage()->ListDirectory(IStorage::TYPE_ALL, "ghosts", GhostlistFetchCallback, this); + + int OwnTime = -1; + int Own = -1; + + for(int i = 0; i < m_lGhosts.size(); i++) + { + if(str_comp(m_lGhosts[i].m_aPlayer, g_Config.m_PlayerName) == 0 && (OwnTime == -1 || m_lGhosts[i].m_Time < OwnTime)) + { + OwnTime = m_lGhosts[i].m_Time; + Own = i; + } + } + + if(Own != -1) + { + m_lGhosts[Own].m_ID = -1; + m_lGhosts[Own].m_Active = true; + m_pClient->m_pGhost->Load(m_lGhosts[Own].m_aFilename, -1); + } +} + +void CMenus::RenderGhost(CUIRect MainView) +{ + // render background + RenderTools()->DrawUIRect(&MainView, ms_ColorTabbarActive, CUI::CORNER_B|CUI::CORNER_TL, 10.0f); + + MainView.HSplitTop(10.0f, 0, &MainView); + MainView.HSplitBottom(5.0f, &MainView, 0); + MainView.VSplitLeft(5.0f, 0, &MainView); + MainView.VSplitRight(5.0f, &MainView, 0); + + CUIRect Headers, Status; + CUIRect View = MainView; + + View.HSplitTop(17.0f, &Headers, &View); + View.HSplitBottom(28.0f, &View, &Status); + + // split of the scrollbar + RenderTools()->DrawUIRect(&Headers, vec4(1,1,1,0.25f), CUI::CORNER_T, 5.0f); + Headers.VSplitRight(20.0f, &Headers, 0); + + struct CColumn + { + int m_Id; + CLocConstString m_Caption; + float m_Width; + CUIRect m_Rect; + CUIRect m_Spacer; + }; + + enum + { + COL_ACTIVE=0, + COL_NAME, + COL_TIME, + }; + + static CColumn s_aCols[] = { + {-1, " ", 2.0f, {0}, {0}}, + {COL_ACTIVE, " ", 30.0f, {0}, {0}}, + {COL_NAME, "Name", 300.0f, {0}, {0}}, + {COL_TIME, "Time", 200.0f, {0}, {0}}, + }; + + int NumCols = sizeof(s_aCols)/sizeof(CColumn); + + // do layout + for(int i = 0; i < NumCols; i++) + { + Headers.VSplitLeft(s_aCols[i].m_Width, &s_aCols[i].m_Rect, &Headers); + + if(i+1 < NumCols) + Headers.VSplitLeft(2, &s_aCols[i].m_Spacer, &Headers); + } + + // do headers + for(int i = 0; i < NumCols; i++) + DoButton_GridHeader(s_aCols[i].m_Caption, s_aCols[i].m_Caption, 0, &s_aCols[i].m_Rect); + + RenderTools()->DrawUIRect(&View, vec4(0,0,0,0.15f), 0, 0); + + CUIRect Scroll; + View.VSplitRight(15, &View, &Scroll); + + int NumGhosts = m_lGhosts.size(); + + int Num = (int)(View.h/s_aCols[0].m_Rect.h) + 1; + static int s_ScrollBar = 0; + static float s_ScrollValue = 0; + + Scroll.HMargin(5.0f, &Scroll); + s_ScrollValue = DoScrollbarV(&s_ScrollBar, &Scroll, s_ScrollValue); + + int ScrollNum = NumGhosts-Num+1; + if(ScrollNum > 0) + { + if(Input()->KeyPresses(KEY_MOUSE_WHEEL_UP)) + s_ScrollValue -= 1.0f/ScrollNum; + if(Input()->KeyPresses(KEY_MOUSE_WHEEL_DOWN)) + s_ScrollValue += 1.0f/ScrollNum; + } + else + ScrollNum = 0; + + static int s_SelectedIndex = 0; + for(int i = 0; i < m_NumInputEvents; i++) + { + int NewIndex = -1; + if(m_aInputEvents[i].m_Flags&IInput::FLAG_PRESS) + { + if(m_aInputEvents[i].m_Key == KEY_DOWN) NewIndex = s_SelectedIndex + 1; + if(m_aInputEvents[i].m_Key == KEY_UP) NewIndex = s_SelectedIndex - 1; + } + if(NewIndex > -1 && NewIndex < NumGhosts) + { + //scroll + float IndexY = View.y - s_ScrollValue*ScrollNum*s_aCols[0].m_Rect.h + NewIndex*s_aCols[0].m_Rect.h; + int Scroll = View.y > IndexY ? -1 : View.y+View.h < IndexY+s_aCols[0].m_Rect.h ? 1 : 0; + if(Scroll) + { + if(Scroll < 0) + { + int NumScrolls = (View.y-IndexY+s_aCols[0].m_Rect.h-1.0f)/s_aCols[0].m_Rect.h; + s_ScrollValue -= (1.0f/ScrollNum)*NumScrolls; + } + else + { + int NumScrolls = (IndexY+s_aCols[0].m_Rect.h-(View.y+View.h)+s_aCols[0].m_Rect.h-1.0f)/s_aCols[0].m_Rect.h; + s_ScrollValue += (1.0f/ScrollNum)*NumScrolls; + } + } + + s_SelectedIndex = NewIndex; + } + } + + if(s_ScrollValue < 0) s_ScrollValue = 0; + if(s_ScrollValue > 1) s_ScrollValue = 1; + + // set clipping + UI()->ClipEnable(&View); + + CUIRect OriginalView = View; + View.y -= s_ScrollValue*ScrollNum*s_aCols[0].m_Rect.h; + + for (int i = 0; i < NumGhosts; i++) + { + const CGhostItem *pItem = &m_lGhosts[i]; + CUIRect Row; + CUIRect SelectHitBox; + + View.HSplitTop(17.0f, &Row, &View); + SelectHitBox = Row; + + // make sure that only those in view can be selected + if(Row.y+Row.h > OriginalView.y && Row.y < OriginalView.y+OriginalView.h) + { + if(i == s_SelectedIndex) + { + CUIRect r = Row; + r.Margin(1.5f, &r); + RenderTools()->DrawUIRect(&r, vec4(1,1,1,0.5f), CUI::CORNER_ALL, 4.0f); + } + + // clip the selection + if(SelectHitBox.y < OriginalView.y) // top + { + SelectHitBox.h -= OriginalView.y-SelectHitBox.y; + SelectHitBox.y = OriginalView.y; + } + else if(SelectHitBox.y+SelectHitBox.h > OriginalView.y+OriginalView.h) // bottom + SelectHitBox.h = OriginalView.y+OriginalView.h-SelectHitBox.y; + + if(UI()->DoButtonLogic(pItem, "", 0, &SelectHitBox)) + { + s_SelectedIndex = i; + } + + if(UI()->MouseInside(&Row) && Input()->MouseDoubleClick()) + { + if(m_lGhosts[s_SelectedIndex].m_Active) + { + m_lGhosts[s_SelectedIndex].m_Active = false; + m_pClient->m_pGhost->Unload(m_lGhosts[s_SelectedIndex].m_ID); + } + else + { + m_lGhosts[s_SelectedIndex].m_Active = true; + m_pClient->m_pGhost->Load(m_lGhosts[s_SelectedIndex].m_aFilename, m_lGhosts[s_SelectedIndex].m_ID); + } + } + } + + for(int c = 0; c < NumCols; c++) + { + CUIRect Button; + Button.x = s_aCols[c].m_Rect.x; + Button.y = Row.y; + Button.h = Row.h; + Button.w = s_aCols[c].m_Rect.w; + + int Id = s_aCols[c].m_Id; + + if(Id == COL_ACTIVE) + { + if(pItem->m_Active) + { + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_EMOTICONS].m_Id); + Graphics()->QuadsBegin(); + RenderTools()->SelectSprite(SPRITE_OOP + 7); + IGraphics::CQuadItem QuadItem(Button.x+Button.w/2, Button.y+Button.h/2, 20.0f, 20.0f); + Graphics()->QuadsDraw(&QuadItem, 1); + + Graphics()->QuadsEnd(); + } + } + else if(Id == COL_NAME) + { + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, Button.x, Button.y, 12.0f * UI()->Scale(), TEXTFLAG_RENDER|TEXTFLAG_STOP_AT_END); + Cursor.m_LineWidth = Button.w; + + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "%s%s", pItem->m_aPlayer, (pItem->m_ID == -1)?" (own)":""); + TextRender()->TextEx(&Cursor, aBuf, -1); + } + else if(Id == COL_TIME) + { + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, Button.x, Button.y, 12.0f * UI()->Scale(), TEXTFLAG_RENDER|TEXTFLAG_STOP_AT_END); + Cursor.m_LineWidth = Button.w; + + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "%02d:%06.3f", (int)pItem->m_Time/60, pItem->m_Time-((int)pItem->m_Time/60*60)); + TextRender()->TextEx(&Cursor, aBuf, -1); + } + } + } + + UI()->ClipDisable(); + + RenderTools()->DrawUIRect(&Status, vec4(1,1,1,0.25f), CUI::CORNER_B, 5.0f); + Status.Margin(5.0f, &Status); + + CUIRect Button; + Status.VSplitRight(120.0f, &Status, &Button); + + static int s_GhostButton = 0; + if(m_lGhosts[s_SelectedIndex].m_Active) + { + if(DoButton_Menu(&s_GhostButton, Localize("Deactivate"), 0, &Button)) + { + m_lGhosts[s_SelectedIndex].m_Active = false; + m_pClient->m_pGhost->Unload(m_lGhosts[s_SelectedIndex].m_ID); + } + } + else + { + if(DoButton_Menu(&s_GhostButton, Localize("Activate"), 0, &Button)) + { + m_lGhosts[s_SelectedIndex].m_Active = true; + m_pClient->m_pGhost->Load(m_lGhosts[s_SelectedIndex].m_aFilename, m_lGhosts[s_SelectedIndex].m_ID); + } + } +} diff --git a/src/game/client/components/race_demo.cpp b/src/game/client/components/race_demo.cpp new file mode 100644 index 000000000..28467dc86 --- /dev/null +++ b/src/game/client/components/race_demo.cpp @@ -0,0 +1,202 @@ +/* (c) Redix and Sushi */ + +#include + +#include +#include +#include + +#include "menus.h" +#include "race_demo.h" + +CRaceDemo::CRaceDemo() +{ + m_RaceState = RACE_NONE; + m_RecordStopTime = 0; + m_Time = 0; + m_DemoStartTick = 0; +} + +void CRaceDemo::OnRender() +{ + if(!g_Config.m_ClAutoRaceRecord || !m_pClient->m_Snap.m_pGameobj || m_pClient->m_Snap.m_Spectate) + { + m_Active = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Active; + return; + } + + // only for race + if(!m_pClient->m_IsRace) + return; + + vec2 PlayerPos = m_pClient->m_LocalCharacterPos; + + // start the demo + if(((!m_Active && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Active)) && m_DemoStartTick < Client()->GameTick()) + { + if(m_RaceState == RACE_STARTED) + OnReset(); + + m_pMap = Client()->RaceRecordStart("tmp"); + m_DemoStartTick = Client()->GameTick() + Client()->GameTickSpeed(); + m_RaceState = RACE_STARTED; + } + + // stop the demo + if(m_RaceState == RACE_FINISHED && m_RecordStopTime < Client()->GameTick() && m_Time > 0) + { + CheckDemo(); + OnReset(); + } + + m_Active = m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalCid].m_Active; +} + +void CRaceDemo::OnReset() +{ + if(Client()->DemoIsRecording()) + Client()->RaceRecordStop(); + + char aFilename[512]; + str_format(aFilename, sizeof(aFilename), "demos/%s_tmp.demo", m_pMap); + Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE); + + m_Time = 0; + m_RaceState = RACE_NONE; + m_RecordStopTime = 0; + m_DemoStartTick = 0; +} + +void CRaceDemo::OnShutdown() +{ + if(Client()->DemoIsRecording()) + Client()->RaceRecordStop(); + + char aFilename[512]; + str_format(aFilename, sizeof(aFilename), "demos/%s_tmp.demo", m_pMap); + Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE); +} + +void CRaceDemo::OnMessage(int MsgType, void *pRawMsg) +{ + if(!g_Config.m_ClAutoRaceRecord || m_pClient->m_Snap.m_Spectate) + return; + + // only for race + if(!m_pClient->m_IsRace) + return; + + // check for messages from server + if(MsgType == NETMSGTYPE_SV_KILLMSG) + { + CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; + if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalCid && m_RaceState == RACE_FINISHED) + { + // check for new record + CheckDemo(); + OnReset(); + } + } + else if(MsgType == NETMSGTYPE_SV_CHAT) + { + CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; + if(pMsg->m_Cid == -1 && m_RaceState == RACE_STARTED) + { + const char* pMessage = pMsg->m_pMessage; + + int Num = 0; + while(str_comp_num(pMessage, " finished in: ", 14)) + { + pMessage++; + Num++; + if(!pMessage[0]) + return; + } + + // store the name + char aName[64]; + str_copy(aName, pMsg->m_pMessage, Num+1); + + // prepare values and state for saving + int Minutes; + float Seconds; + if(!str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName) && sscanf(pMessage, " finished in: %d minute(s) %f", &Minutes, &Seconds) == 2) + { + m_RaceState = RACE_FINISHED; + m_RecordStopTime = Client()->GameTick() + Client()->GameTickSpeed(); + m_Time = Minutes*60 + Seconds; + } + } + } +} + +void CRaceDemo::CheckDemo() +{ + // stop the demo recording + Client()->RaceRecordStop(); + + char aTmpDemoName[128]; + str_format(aTmpDemoName, sizeof(aTmpDemoName), "%s_tmp", m_pMap); + + // loop through demo files + m_pClient->m_pMenus->DemolistPopulate(); + for(int i = 0; i < m_pClient->m_pMenus->m_lDemos.size(); i++) + { + if(!str_comp_num(m_pClient->m_pMenus->m_lDemos[i].m_aName, m_pMap, str_length(m_pMap)) && str_comp_num(m_pClient->m_pMenus->m_lDemos[i].m_aName, aTmpDemoName, str_length(aTmpDemoName))) + { + const char *pDemo = m_pClient->m_pMenus->m_lDemos[i].m_aName; + + // set cursor + pDemo += str_length(m_pMap)+1; + float DemoTime = str_tofloat(pDemo); + if(m_Time < DemoTime) + { + // save new record + SaveDemo(m_pMap); + + // delete old demo + char aFilename[512]; + str_format(aFilename, sizeof(aFilename), "demos/%s.demo", m_pClient->m_pMenus->m_lDemos[i].m_aName); + Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE); + } + + m_Time = 0; + + return; + } + } + + // save demo if there is none + SaveDemo(m_pMap); + + m_Time = 0; +} + +void CRaceDemo::SaveDemo(const char* pDemo) +{ + char aNewFilename[512]; + char aOldFilename[512]; + if(g_Config.m_ClDemoName) + { + char aPlayerName[MAX_NAME_LENGTH]; + str_copy(aPlayerName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalCid].m_aName, sizeof(aPlayerName)); + + // check the player name + for(int i = 0; i < MAX_NAME_LENGTH; i++) + { + if(!aPlayerName[i]) + break; + + if(aPlayerName[i] == '\\' || aPlayerName[i] == '/' || aPlayerName[i] == '|' || aPlayerName[i] == ':' || aPlayerName[i] == '*' || aPlayerName[i] == '?' || aPlayerName[i] == '<' || aPlayerName[i] == '>' || aPlayerName[i] == '"') + aPlayerName[i] = '%'; + + str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%5.2f_%s.demo", pDemo, m_Time, aPlayerName); + } + } + else + str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%5.2f.demo", pDemo, m_Time); + + str_format(aOldFilename, sizeof(aOldFilename), "demos/%s_tmp.demo", m_pMap); + + Storage()->RenameFile(aOldFilename, aNewFilename, IStorage::TYPE_SAVE); +} diff --git a/src/game/client/components/race_demo.h b/src/game/client/components/race_demo.h new file mode 100644 index 000000000..721b62687 --- /dev/null +++ b/src/game/client/components/race_demo.h @@ -0,0 +1,40 @@ +/* (c) Redix and Sushi */ + +#ifndef GAME_CLIENT_COMPONENTS_RACE_DEMO_H +#define GAME_CLIENT_COMPONENTS_RACE_DEMO_H + +#include + +#include + +class CRaceDemo : public CComponent +{ + int m_RecordStopTime; + int m_DemoStartTick; + float m_Time; + const char *m_pMap; + + bool m_Active; + +public: + + int m_RaceState; + + enum + { + RACE_NONE = 0, + RACE_STARTED, + RACE_FINISHED, + }; + + CRaceDemo(); + + virtual void OnReset(); + virtual void OnRender(); + virtual void OnShutdown(); + virtual void OnMessage(int MsgType, void *pRawMsg); + + void CheckDemo(); + void SaveDemo(const char* pDemo); +}; +#endif diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 25a1088af..185ba1426 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -44,6 +44,8 @@ #include "components/skins.h" #include "components/sounds.h" #include "components/voting.h" +#include "components/race_demo.h" +#include "components/ghost.h" #include CGameClient g_GameClient; @@ -69,6 +71,8 @@ static CSounds gs_Sounds; static CEmoticon gs_Emoticon; static CDamageInd gsDamageInd; static CVoting gs_Voting; +static CRaceDemo gs_RaceDemo; +static CGhost gs_Ghost; static CPlayers gs_Players; static CNamePlates gs_NamePlates; @@ -141,6 +145,9 @@ void CGameClient::OnConsoleInit() m_pMapimages = &::gs_MapImages; m_pVoting = &::gs_Voting; m_pScoreboard = &::gs_Scoreboard; + + m_pRaceDemo = &::gs_RaceDemo; + m_pGhost = &::gs_Ghost; // make a list of all the systems, make sure to add them in the corrent render order m_All.Add(m_pSkins); @@ -153,11 +160,13 @@ void CGameClient::OnConsoleInit() m_All.Add(m_pSounds); m_All.Add(m_pVoting); m_All.Add(m_pParticles); // doesn't render anything, just updates all the particles + m_All.Add(m_pRaceDemo); m_All.Add(&gs_MapLayersBackGround); // first to render m_All.Add(&m_pParticles->m_RenderTrail); m_All.Add(&gs_Items); m_All.Add(&gs_Players); + m_All.Add(m_pGhost); m_All.Add(&gs_MapLayersForeGround); m_All.Add(&m_pParticles->m_RenderExplosions); m_All.Add(&gs_NamePlates); @@ -308,6 +317,7 @@ void CGameClient::OnInit() m_ServerMode = SERVERMODE_PURE; + m_IsRace = false; m_DDRaceMsgSent = false; } @@ -397,6 +407,7 @@ void CGameClient::OnReset() m_All.m_paComponents[i]->OnReset(); m_Teams.Reset(); + m_IsRace = false; m_DDRaceMsgSent = false; } @@ -623,7 +634,11 @@ void CGameClient::OnStateChange(int NewState, int OldState) m_All.m_paComponents[i]->OnStateChange(NewState, OldState); } -void CGameClient::OnShutdown() {} +void CGameClient::OnShutdown() +{ + m_pRaceDemo->OnShutdown(); +} + void CGameClient::OnEnterGame() {} void CGameClient::OnGameOver() @@ -889,6 +904,8 @@ void CGameClient::OnNewSnapshot() CNetMsg_Cl_IsDDRace Msg; Client()->SendPackMsg(&Msg, MSGFLAG_VITAL); m_DDRaceMsgSent = true; + if(!m_IsRace) + m_IsRace = true; } } diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 2baf936b2..b6b15990f 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -218,6 +218,12 @@ public: class CMapImages *m_pMapimages; class CVoting *m_pVoting; class CScoreboard *m_pScoreboard; + + //DDRace + //TODO: This is ugly + class CRaceDemo *m_pRaceDemo; + class CGhost *m_pGhost; + bool m_IsRace; }; diff --git a/src/game/client/render.cpp b/src/game/client/render.cpp index 68d62ac5a..02eddaeb8 100644 --- a/src/game/client/render.cpp +++ b/src/game/client/render.cpp @@ -165,7 +165,7 @@ void CRenderTools::DrawUIRect(const CUIRect *r, vec4 Color, int Corners, float R Graphics()->QuadsEnd(); } -void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos) +void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, bool Alpha) { vec2 Direction = Dir; vec2 Position = Pos; @@ -192,7 +192,10 @@ void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle*pi*2); // draw body - Graphics()->SetColor(pInfo->m_ColorBody.r, pInfo->m_ColorBody.g, pInfo->m_ColorBody.b, 1.0f); + if(Alpha) + Graphics()->SetColor(pInfo->m_ColorBody.r, pInfo->m_ColorBody.g, pInfo->m_ColorBody.b, pInfo->m_ColorBody.a); + else + Graphics()->SetColor(pInfo->m_ColorBody.r, pInfo->m_ColorBody.g, pInfo->m_ColorBody.b, 1.0f); vec2 BodyPos = Position + vec2(pAnim->GetBody()->m_X, pAnim->GetBody()->m_Y)*AnimScale; SelectSprite(OutLine?SPRITE_TEE_BODY_OUTLINE:SPRITE_TEE_BODY, 0, 0, 0); IGraphics::CQuadItem QuadItem(BodyPos.x, BodyPos.y, BaseSize, BaseSize); @@ -250,8 +253,11 @@ void CRenderTools::RenderTee(CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote if(Indicate) cs = 0.5f; } - - Graphics()->SetColor(pInfo->m_ColorFeet.r*cs, pInfo->m_ColorFeet.g*cs, pInfo->m_ColorFeet.b*cs, 1.0f); + + if(Alpha) + Graphics()->SetColor(pInfo->m_ColorFeet.r*cs, pInfo->m_ColorFeet.g*cs, pInfo->m_ColorFeet.b*cs, pInfo->m_ColorFeet.a*cs); + else + Graphics()->SetColor(pInfo->m_ColorFeet.r*cs, pInfo->m_ColorFeet.g*cs, pInfo->m_ColorFeet.b*cs, 1.0f); IGraphics::CQuadItem QuadItem(Position.x+pFoot->m_X*AnimScale, Position.y+pFoot->m_Y*AnimScale, w, h); Graphics()->QuadsDraw(&QuadItem, 1); } diff --git a/src/game/client/render.h b/src/game/client/render.h index 5e6d74bc3..53f643e66 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -66,7 +66,7 @@ public: void RenderTilemapGenerateSkip(class CLayers *pLayers); // object render methods (gc_render_obj.cpp) - void RenderTee(class CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos); + void RenderTee(class CAnimState *pAnim, CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, bool Alpha=false); // map render methods (gc_render_map.cpp) static void RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Channels, float Time, float *pResult);